code.gitea.io/gitea@v1.22.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, objectFormatName string) error { 67 err := os.MkdirAll(repoPath, os.ModePerm) 68 if err != nil { 69 return err 70 } 71 72 cmd := NewCommand(ctx, "init") 73 74 if !IsValidObjectFormat(objectFormatName) { 75 return fmt.Errorf("invalid object format: %s", objectFormatName) 76 } 77 if DefaultFeatures().SupportHashSha256 { 78 cmd.AddOptionValues("--object-format", objectFormatName) 79 } 80 81 if bare { 82 cmd.AddArguments("--bare") 83 } 84 _, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath}) 85 return err 86 } 87 88 // IsEmpty Check if repository is empty. 89 func (repo *Repository) IsEmpty() (bool, error) { 90 var errbuf, output strings.Builder 91 if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all"). 92 Run(&RunOpts{ 93 Dir: repo.Path, 94 Stdout: &output, 95 Stderr: &errbuf, 96 }); err != nil { 97 if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" { 98 // git 2.11 exits with 129 if the repo is empty 99 return true, nil 100 } 101 return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String()) 102 } 103 104 return strings.TrimSpace(output.String()) == "", nil 105 } 106 107 // CloneRepoOptions options when clone a repository 108 type CloneRepoOptions struct { 109 Timeout time.Duration 110 Mirror bool 111 Bare bool 112 Quiet bool 113 Branch string 114 Shared bool 115 NoCheckout bool 116 Depth int 117 Filter string 118 SkipTLSVerify bool 119 } 120 121 // Clone clones original repository to target path. 122 func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { 123 return CloneWithArgs(ctx, globalCommandArgs, from, to, opts) 124 } 125 126 // CloneWithArgs original repository to target path. 127 func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) { 128 toDir := path.Dir(to) 129 if err = os.MkdirAll(toDir, os.ModePerm); err != nil { 130 return err 131 } 132 133 cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone") 134 if opts.SkipTLSVerify { 135 cmd.AddArguments("-c", "http.sslVerify=false") 136 } 137 if opts.Mirror { 138 cmd.AddArguments("--mirror") 139 } 140 if opts.Bare { 141 cmd.AddArguments("--bare") 142 } 143 if opts.Quiet { 144 cmd.AddArguments("--quiet") 145 } 146 if opts.Shared { 147 cmd.AddArguments("-s") 148 } 149 if opts.NoCheckout { 150 cmd.AddArguments("--no-checkout") 151 } 152 if opts.Depth > 0 { 153 cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth)) 154 } 155 if opts.Filter != "" { 156 cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter) 157 } 158 if len(opts.Branch) > 0 { 159 cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) 160 } 161 cmd.AddDashesAndList(from, to) 162 163 if strings.Contains(from, "://") && strings.Contains(from, "@") { 164 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)) 165 } else { 166 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)) 167 } 168 169 if opts.Timeout <= 0 { 170 opts.Timeout = -1 171 } 172 173 envs := os.Environ() 174 u, err := url.Parse(from) 175 if err == nil { 176 envs = proxy.EnvWithProxy(u) 177 } 178 179 stderr := new(bytes.Buffer) 180 if err = cmd.Run(&RunOpts{ 181 Timeout: opts.Timeout, 182 Env: envs, 183 Stdout: io.Discard, 184 Stderr: stderr, 185 }); err != nil { 186 return ConcatenateError(err, stderr.String()) 187 } 188 return nil 189 } 190 191 // PushOptions options when push to remote 192 type PushOptions struct { 193 Remote string 194 Branch string 195 Force bool 196 Mirror bool 197 Env []string 198 Timeout time.Duration 199 } 200 201 // Push pushs local commits to given remote branch. 202 func Push(ctx context.Context, repoPath string, opts PushOptions) error { 203 cmd := NewCommand(ctx, "push") 204 if opts.Force { 205 cmd.AddArguments("-f") 206 } 207 if opts.Mirror { 208 cmd.AddArguments("--mirror") 209 } 210 remoteBranchArgs := []string{opts.Remote} 211 if len(opts.Branch) > 0 { 212 remoteBranchArgs = append(remoteBranchArgs, opts.Branch) 213 } 214 cmd.AddDashesAndList(remoteBranchArgs...) 215 216 if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") { 217 cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror)) 218 } else { 219 cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror)) 220 } 221 222 stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) 223 if err != nil { 224 if strings.Contains(stderr, "non-fast-forward") { 225 return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err} 226 } else if strings.Contains(stderr, "! [remote rejected]") { 227 err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err} 228 err.GenerateMessage() 229 return err 230 } else if strings.Contains(stderr, "matches more than one") { 231 return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err} 232 } 233 return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout) 234 } 235 236 return nil 237 } 238 239 // GetLatestCommitTime returns time for latest commit in repository (across all branches) 240 func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) { 241 cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") 242 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 243 if err != nil { 244 return time.Time{}, err 245 } 246 commitTime := strings.TrimSpace(stdout) 247 return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) 248 } 249 250 // DivergeObject represents commit count diverging commits 251 type DivergeObject struct { 252 Ahead int 253 Behind int 254 } 255 256 // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch 257 func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { 258 cmd := NewCommand(ctx, "rev-list", "--count", "--left-right"). 259 AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") 260 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 261 if err != nil { 262 return do, err 263 } 264 left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t") 265 if !found { 266 return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout) 267 } 268 269 do.Behind, err = strconv.Atoi(left) 270 if err != nil { 271 return do, err 272 } 273 do.Ahead, err = strconv.Atoi(right) 274 if err != nil { 275 return do, err 276 } 277 return do, nil 278 } 279 280 // CreateBundle create bundle content to the target path 281 func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error { 282 tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle") 283 if err != nil { 284 return err 285 } 286 defer os.RemoveAll(tmp) 287 288 env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects")) 289 _, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env}) 290 if err != nil { 291 return err 292 } 293 294 _, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env}) 295 if err != nil { 296 return err 297 } 298 299 _, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env}) 300 if err != nil { 301 return err 302 } 303 304 tmpFile := filepath.Join(tmp, "bundle") 305 _, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env}) 306 if err != nil { 307 return err 308 } 309 310 fi, err := os.Open(tmpFile) 311 if err != nil { 312 return err 313 } 314 defer fi.Close() 315 316 _, err = io.Copy(out, fi) 317 return err 318 }