mirror of
1
Fork 0
forgejo/modules/git/repo.go

459 lines
12 KiB
Go
Raw Normal View History

2016-11-03 23:16:01 +01:00
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2017 The Gitea Authors. All rights reserved.
2016-11-03 23:16:01 +01:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package git
import (
"bytes"
"context"
"fmt"
"io"
"net/url"
2016-11-03 23:16:01 +01:00
"os"
"path"
"path/filepath"
"strconv"
"strings"
2016-11-03 23:16:01 +01:00
"time"
"code.gitea.io/gitea/modules/proxy"
2016-11-03 23:16:01 +01:00
)
Sign merges, CRUD, Wiki and Repository initialisation with gpg key (#7631) This PR fixes #7598 by providing a configurable way of signing commits across the Gitea instance. Per repository configurability and import/generation of trusted secure keys is not provided by this PR - from a security PoV that's probably impossible to do properly. Similarly web-signing, that is asking the user to sign something, is not implemented - this could be done at a later stage however. ## Features - [x] If commit.gpgsign is set in .gitconfig sign commits and files created through repofiles. (merges should already have been signed.) - [x] Verify commits signed with the default gpg as valid - [x] Signer, Committer and Author can all be different - [x] Allow signer to be arbitrarily different - We still require the key to have an activated email on Gitea. A more complete implementation would be to use a keyserver and mark external-or-unactivated with an "unknown" trust level icon. - [x] Add a signing-key.gpg endpoint to get the default gpg pub key if available - Rather than add a fake web-flow user I've added this as an endpoint on /api/v1/signing-key.gpg - [x] Try to match the default key with a user on gitea - this is done at verification time - [x] Make things configurable? - app.ini configuration done - [x] when checking commits are signed need to check if they're actually verifiable too - [x] Add documentation I have decided that adjusting the docker to create a default gpg key is not the correct thing to do and therefore have not implemented this.
2019-10-16 15:42:42 +02:00
// GPGSettings represents the default GPG settings for this repository
type GPGSettings struct {
Sign bool
KeyID string
Email string
Name string
PublicKeyContent string
2016-11-03 23:16:01 +01:00
}
2016-12-22 10:30:52 +01:00
const prettyLogFormat = `--pretty=format:%H`
2016-11-03 23:16:01 +01:00
// GetAllCommitsCount returns count of all commits in repository
func (repo *Repository) GetAllCommitsCount() (int64, error) {
Multiple GitGraph improvements: Exclude PR heads, Add branch/PR links, Show only certain branches, (#12766) * Multiple GitGraph improvements. Add backend support for excluding PRs, selecting branches and files. Fix #10327 Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * Only show refs in dropdown we display on the graph Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @silverwind Signed-off-by: Andrew Thornton <art27@cantab.net> * use flexbox for ui header Signed-off-by: Andrew Thornton <art27@cantab.net> * Move Hide Pull Request button to the dropdown Signed-off-by: Andrew Thornton <art27@cantab.net> * Add SHA and user pictures Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test Signed-off-by: Andrew Thornton <art27@cantab.net> * fix test 2 Signed-off-by: Andrew Thornton <art27@cantab.net> * fixes * async * more tweaks * use tabs in tmpl Signed-off-by: Andrew Thornton <art27@cantab.net> * remove commented thing Signed-off-by: Andrew Thornton <art27@cantab.net> * fix linting Signed-off-by: Andrew Thornton <art27@cantab.net> * Update web_src/js/features/gitgraph.js Co-authored-by: silverwind <me@silverwind.io> * graph tweaks * more tweaks * add title Signed-off-by: Andrew Thornton <art27@cantab.net> * fix loading indicator z-index and position Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: techknowlogick <techknowlogick@gitea.io> Co-authored-by: Lauris BH <lauris@nix.lv>
2020-11-08 18:21:54 +01:00
return AllCommitsCount(repo.Path, false)
}
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
var commits []*Commit
2016-11-03 23:16:01 +01:00
if len(logs) == 0 {
return commits, nil
2016-11-03 23:16:01 +01:00
}
parts := bytes.Split(logs, []byte{'\n'})
2016-12-22 10:30:52 +01:00
for _, commitID := range parts {
commit, err := repo.GetCommit(string(commitID))
2016-11-03 23:16:01 +01:00
if err != nil {
return nil, err
}
commits = append(commits, commit)
2016-11-03 23:16:01 +01:00
}
return commits, nil
2016-11-03 23:16:01 +01:00
}
// IsRepoURLAccessible checks if given repository URL is accessible.
func IsRepoURLAccessible(url string) bool {
_, err := NewCommand("ls-remote", "-q", "-h", url, "HEAD").Run()
2019-06-12 21:41:28 +02:00
return err == nil
2016-11-03 23:16:01 +01:00
}
// InitRepository initializes a new Git repository.
func InitRepository(repoPath string, bare bool) error {
2019-06-12 21:41:28 +02:00
err := os.MkdirAll(repoPath, os.ModePerm)
if err != nil {
return err
}
2016-11-03 23:16:01 +01:00
cmd := NewCommand("init")
if bare {
cmd.AddArguments("--bare")
}
2019-06-12 21:41:28 +02:00
_, err = cmd.RunInDir(repoPath)
2016-11-03 23:16:01 +01:00
return err
}
// IsEmpty Check if repository is empty.
func (repo *Repository) IsEmpty() (bool, error) {
var errbuf strings.Builder
if err := NewCommand("log", "-1").RunInDirPipeline(repo.Path, nil, &errbuf); err != nil {
if strings.Contains(errbuf.String(), "fatal: bad default revision 'HEAD'") ||
strings.Contains(errbuf.String(), "fatal: your current branch 'master' does not have any commits yet") {
return true, nil
}
return true, fmt.Errorf("check empty: %v - %s", err, errbuf.String())
}
return false, nil
}
2016-12-22 10:30:52 +01:00
// CloneRepoOptions options when clone a repository
2016-11-03 23:16:01 +01:00
type CloneRepoOptions struct {
2019-05-11 17:29:17 +02:00
Timeout time.Duration
Mirror bool
Bare bool
Quiet bool
Branch string
Shared bool
NoCheckout bool
Depth int
2016-11-03 23:16:01 +01:00
}
// Clone clones original repository to target path.
func Clone(from, to string, opts CloneRepoOptions) error {
return CloneWithContext(DefaultContext, from, to, opts)
}
// CloneWithContext clones original repository to target path.
func CloneWithContext(ctx context.Context, from, to string, opts CloneRepoOptions) error {
cargs := make([]string, len(GlobalCommandArgs))
copy(cargs, GlobalCommandArgs)
return CloneWithArgs(ctx, from, to, cargs, opts)
}
// CloneWithArgs original repository to target path.
func CloneWithArgs(ctx context.Context, from, to string, args []string, opts CloneRepoOptions) (err error) {
2016-11-03 23:16:01 +01:00
toDir := path.Dir(to)
if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
return err
}
cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone")
2016-11-03 23:16:01 +01:00
if opts.Mirror {
cmd.AddArguments("--mirror")
}
if opts.Bare {
cmd.AddArguments("--bare")
}
if opts.Quiet {
cmd.AddArguments("--quiet")
}
2019-05-11 17:29:17 +02:00
if opts.Shared {
cmd.AddArguments("-s")
}
if opts.NoCheckout {
cmd.AddArguments("--no-checkout")
}
if opts.Depth > 0 {
cmd.AddArguments("--depth", strconv.Itoa(opts.Depth))
}
2019-05-11 17:29:17 +02:00
2016-11-03 23:16:01 +01:00
if len(opts.Branch) > 0 {
cmd.AddArguments("-b", opts.Branch)
}
2019-05-11 17:29:17 +02:00
cmd.AddArguments("--", from, to)
2016-11-03 23:16:01 +01:00
if opts.Timeout <= 0 {
opts.Timeout = -1
}
var envs = os.Environ()
u, err := url.Parse(from)
if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) {
if proxy.Match(u.Host) {
envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL()))
}
}
var stderr = new(bytes.Buffer)
if err = cmd.RunWithContext(&RunContext{
Timeout: opts.Timeout,
Env: envs,
Stdout: io.Discard,
Stderr: stderr,
}); err != nil {
return ConcatenateError(err, stderr.String())
}
return nil
2016-11-03 23:16:01 +01:00
}
2016-12-22 10:30:52 +01:00
// PullRemoteOptions options when pull from remote
2016-11-03 23:16:01 +01:00
type PullRemoteOptions struct {
Timeout time.Duration
All bool
2016-11-12 12:09:25 +01:00
Rebase bool
2016-11-03 23:16:01 +01:00
Remote string
Branch string
}
// Pull pulls changes from remotes.
func Pull(repoPath string, opts PullRemoteOptions) error {
cmd := NewCommand("pull")
2016-11-12 12:09:25 +01:00
if opts.Rebase {
cmd.AddArguments("--rebase")
}
2016-11-03 23:16:01 +01:00
if opts.All {
cmd.AddArguments("--all")
} else {
cmd.AddArguments("--", opts.Remote, opts.Branch)
2016-11-03 23:16:01 +01:00
}
if opts.Timeout <= 0 {
opts.Timeout = -1
}
_, err := cmd.RunInDirTimeout(opts.Timeout, repoPath)
return err
}
// PushOptions options when push to remote
type PushOptions struct {
Remote string
Branch string
Force bool
Mirror bool
Env []string
Timeout time.Duration
}
2016-11-03 23:16:01 +01:00
// Push pushs local commits to given remote branch.
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
cmd := NewCommandContext(ctx, "push")
if opts.Force {
cmd.AddArguments("-f")
}
if opts.Mirror {
cmd.AddArguments("--mirror")
}
cmd.AddArguments("--", opts.Remote)
if len(opts.Branch) > 0 {
cmd.AddArguments(opts.Branch)
}
var outbuf, errbuf strings.Builder
if opts.Timeout == 0 {
opts.Timeout = -1
}
err := cmd.RunInDirTimeoutEnvPipeline(opts.Env, opts.Timeout, repoPath, &outbuf, &errbuf)
if err != nil {
if strings.Contains(errbuf.String(), "non-fast-forward") {
return &ErrPushOutOfDate{
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
} else if strings.Contains(errbuf.String(), "! [remote rejected]") {
err := &ErrPushRejected{
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
err.GenerateMessage()
return err
} else if strings.Contains(errbuf.String(), "matches more than one") {
err := &ErrMoreThanOne{
StdOut: outbuf.String(),
StdErr: errbuf.String(),
Err: err,
}
return err
}
}
if errbuf.Len() > 0 && err != nil {
return fmt.Errorf("%v - %s", err, errbuf.String())
}
2016-11-03 23:16:01 +01:00
return err
}
2016-12-22 10:30:52 +01:00
// CheckoutOptions options when heck out some branch
2016-11-03 23:16:01 +01:00
type CheckoutOptions struct {
Timeout time.Duration
Branch string
OldBranch string
}
// Checkout checkouts a branch
func Checkout(repoPath string, opts CheckoutOptions) error {
cmd := NewCommand("checkout")
if len(opts.OldBranch) > 0 {
cmd.AddArguments("-b")
}
if opts.Timeout <= 0 {
opts.Timeout = -1
}
cmd.AddArguments(opts.Branch)
if len(opts.OldBranch) > 0 {
cmd.AddArguments(opts.OldBranch)
}
_, err := cmd.RunInDirTimeout(opts.Timeout, repoPath)
return err
}
// ResetHEAD resets HEAD to given revision or head of branch.
func ResetHEAD(repoPath string, hard bool, revision string) error {
cmd := NewCommand("reset")
if hard {
cmd.AddArguments("--hard")
}
_, err := cmd.AddArguments(revision).RunInDir(repoPath)
return err
}
// MoveFile moves a file to another file or directory.
func MoveFile(repoPath, oldTreeName, newTreeName string) error {
_, err := NewCommand("mv").AddArguments(oldTreeName, newTreeName).RunInDir(repoPath)
return err
}
// CountObject represents repository count objects report
type CountObject struct {
Count int64
Size int64
InPack int64
Packs int64
SizePack int64
PrunePack int64
Garbage int64
SizeGarbage int64
}
const (
statCount = "count: "
statSize = "size: "
statInpack = "in-pack: "
statPacks = "packs: "
statSizePack = "size-pack: "
statPrunePackage = "prune-package: "
statGarbage = "garbage: "
statSizeGarbage = "size-garbage: "
)
// CountObjects returns the results of git count-objects on the repoPath
func CountObjects(repoPath string) (*CountObject, error) {
cmd := NewCommand("count-objects", "-v")
stdout, err := cmd.RunInDir(repoPath)
if err != nil {
return nil, err
}
return parseSize(stdout), nil
}
// parseSize parses the output from count-objects and return a CountObject
func parseSize(objects string) *CountObject {
repoSize := new(CountObject)
for _, line := range strings.Split(objects, "\n") {
switch {
case strings.HasPrefix(line, statCount):
repoSize.Count, _ = strconv.ParseInt(line[7:], 10, 64)
case strings.HasPrefix(line, statSize):
repoSize.Size, _ = strconv.ParseInt(line[6:], 10, 64)
repoSize.Size *= 1024
case strings.HasPrefix(line, statInpack):
repoSize.InPack, _ = strconv.ParseInt(line[9:], 10, 64)
case strings.HasPrefix(line, statPacks):
repoSize.Packs, _ = strconv.ParseInt(line[7:], 10, 64)
case strings.HasPrefix(line, statSizePack):
repoSize.Count, _ = strconv.ParseInt(line[11:], 10, 64)
repoSize.Count *= 1024
case strings.HasPrefix(line, statPrunePackage):
repoSize.PrunePack, _ = strconv.ParseInt(line[16:], 10, 64)
case strings.HasPrefix(line, statGarbage):
repoSize.Garbage, _ = strconv.ParseInt(line[9:], 10, 64)
case strings.HasPrefix(line, statSizeGarbage):
repoSize.SizeGarbage, _ = strconv.ParseInt(line[14:], 10, 64)
repoSize.SizeGarbage *= 1024
}
}
return repoSize
}
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
func GetLatestCommitTime(repoPath string) (time.Time, error) {
cmd := NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
stdout, err := cmd.RunInDir(repoPath)
if err != nil {
return time.Time{}, err
}
commitTime := strings.TrimSpace(stdout)
2017-12-11 03:23:34 +01:00
return time.Parse(GitTimeLayout, commitTime)
}
// DivergeObject represents commit count diverging commits
type DivergeObject struct {
Ahead int
Behind int
}
func checkDivergence(repoPath string, baseBranch string, targetBranch string) (int, error) {
branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch)
cmd := NewCommand("rev-list", "--count", branches)
stdout, err := cmd.RunInDir(repoPath)
if err != nil {
return -1, err
}
outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n"))
if errInteger != nil {
return -1, errInteger
}
return outInteger, nil
}
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
func GetDivergingCommits(repoPath string, baseBranch string, targetBranch string) (DivergeObject, error) {
// $(git rev-list --count master..feature) commits ahead of master
ahead, errorAhead := checkDivergence(repoPath, baseBranch, targetBranch)
if errorAhead != nil {
return DivergeObject{}, errorAhead
}
// $(git rev-list --count feature..master) commits behind master
behind, errorBehind := checkDivergence(repoPath, targetBranch, baseBranch)
if errorBehind != nil {
return DivergeObject{}, errorBehind
}
return DivergeObject{ahead, behind}, nil
}
// CreateBundle create bundle content to the target path
func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
_, err = NewCommandContext(ctx, "init", "--bare").RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
_, err = NewCommandContext(ctx, "reset", "--soft", commit).RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
_, err = NewCommandContext(ctx, "branch", "-m", "bundle").RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
tmpFile := filepath.Join(tmp, "bundle")
_, err = NewCommandContext(ctx, "bundle", "create", tmpFile, "bundle", "HEAD").RunInDirWithEnv(tmp, env)
if err != nil {
return err
}
fi, err := os.Open(tmpFile)
if err != nil {
return err
}
defer fi.Close()
_, err = io.Copy(out, fi)
return err
}