diff --git a/.drone.yml b/.drone.yml index 662529bf2eb64..6d9cde5ca9539 100644 --- a/.drone.yml +++ b/.drone.yml @@ -126,9 +126,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.sqlite.test)' | sh)) & - - make test-sqlite-migration - - make test-sqlite + - timeout -s ABRT 20m make test-sqlite-migration + - timeout -s ABRT 20m make test-sqlite when: event: [ push, tag, pull_request ] @@ -158,9 +157,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.test)' | sh)) & - - make test-mysql-migration - - make test-mysql + - timeout -s ABRT 20m make test-mysql-migration + - timeout -s ABRT 20m make test-mysql when: event: [ tag ] @@ -174,9 +172,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.test)' | sh)) & - - make test-mysql8-migration - - make test-mysql8 + - timeout -s ABRT 20m make test-mysql8-migration + - timeout -s ABRT 20m make test-mysql8 when: event: [ push, tag, pull_request ] @@ -190,9 +187,8 @@ pipeline: commands: - curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | bash - apt-get install -y git-lfs - - (sleep 1200 && (echo 'kill -ABRT $(pidof gitea) $(pidof integrations.test)' | sh)) & - - make test-pgsql-migration - - make test-pgsql + - timeout -s ABRT 20m make test-pgsql-migration + - timeout -s ABRT 20m make test-pgsql when: event: [ push, tag, pull_request ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 361ec205460b6..3b7547e1ac848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ This changelog goes through all the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.io). +## [1.8.2](https://github.com/go-gitea/gitea/releases/tag/v1.8.2) - 2019-05-29 +* BUGFIXES + * Fix possbile mysql invalid connnection error (#7051) (#7071) + * Handle invalid administrator username on install page (#7060) (#7063) + * Disable arm7 builds (#7037) (#7042) + * Fix default for allowing new organization creation for new users (#7017) (#7034) + * SearchRepositoryByName improvements and unification (#6897) (#7002) + * Fix u2f registrationlist ToRegistrations() method (#6980) (#6982) + * Allow collaborators to view repo owned by private org (#6965) (#6968) + * Use AppURL for Oauth user link (#6894) (#6925) + * Escape the commit message on issues update (#6901) (#6902) + * Fix regression for API users search (#6882) (#6885) + * Handle early git version's lack of get-url (#7065) (#7076) + * Fix wrong init dependency on markup extensions (#7038) (#7074) + ## [1.8.1](https://github.com/go-gitea/gitea/releases/tag/v1.8.1) - 2019-05-08 * BUGFIXES * Fix 404 when sending pull requests in some situations (#6871) (#6873) diff --git a/Makefile b/Makefile index 718836be65c75..cf1a6458055b3 100644 --- a/Makefile +++ b/Makefile @@ -335,7 +335,7 @@ release-linux: @hash xgo > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ $(GO) get -u src.techknowlogick.com/xgo; \ fi - xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/*' -out gitea-$(VERSION) . + xgo -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/mips64le,linux/mips,linux/mipsle' -out gitea-$(VERSION) . ifeq ($(CI),drone) cp /build/* $(DIST)/binaries endif diff --git a/cmd/hook.go b/cmd/hook.go index 0db17212ad9e3..1ef29907bb314 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -8,15 +8,14 @@ import ( "bufio" "bytes" "fmt" + "net/http" "os" "strconv" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" - "code.gitea.io/gitea/modules/util" "github.com/urfave/cli" ) @@ -62,12 +61,10 @@ func runHookPreReceive(c *cli.Context) error { setup("hooks/pre-receive.log") // the environment setted on serv command - repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") username := os.Getenv(models.EnvRepoUsername) reponame := os.Getenv(models.EnvRepoName) - userIDStr := os.Getenv(models.EnvPusherID) - repoPath := models.RepoPath(username, reponame) + userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64) buf := bytes.NewBuffer(nil) @@ -92,38 +89,20 @@ func runHookPreReceive(c *cli.Context) error { // If the ref is a branch, check if it's protected if strings.HasPrefix(refFullName, git.BranchPrefix) { - branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) - protectBranch, err := private.GetProtectedBranchBy(repoID, branchName) - if err != nil { - fail("Internal error", fmt.Sprintf("retrieve protected branches information failed: %v", err)) - } - - if protectBranch != nil && protectBranch.IsProtected() { - // check and deletion - if newCommitID == git.EmptySHA { - fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "") - } - - // detect force push - if git.EmptySHA != oldCommitID { - output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDir(repoPath) - if err != nil { - fail("Internal error", "Fail to detect force push: %v", err) - } else if len(output) > 0 { - fail(fmt.Sprintf("branch %s is protected from force push", branchName), "") - } - } - - userID, _ := strconv.ParseInt(userIDStr, 10, 64) - canPush, err := private.CanUserPush(protectBranch.ID, userID) - if err == nil && !canPush { - canPush, err = private.HasEnoughApprovals(protectBranch.ID, prID) - } - if err != nil { - fail("Internal error", "Fail to detect user can push: %v", err) - } else if !canPush { - fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "") - } + statusCode, msg := private.HookPreReceive(username, reponame, private.HookOptions{ + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + RefFullName: refFullName, + UserID: userID, + GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories), + GitObjectDirectory: os.Getenv(private.GitObjectDirectory), + ProtectedBranchPRID: prID, + }) + switch statusCode { + case http.StatusInternalServerError: + fail("Internal Server Error", msg) + case http.StatusForbidden: + fail(msg, "") } } } @@ -149,7 +128,6 @@ func runHookPostReceive(c *cli.Context) error { setup("hooks/post-receive.log") // the environment setted on serv command - repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64) repoUser := os.Getenv(models.EnvRepoUsername) isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true") repoName := os.Getenv(models.EnvRepoName) @@ -176,64 +154,31 @@ func runHookPostReceive(c *cli.Context) error { newCommitID := string(fields[1]) refFullName := string(fields[2]) - // Only trigger activity updates for changes to branches or - // tags. Updates to other refs (eg, refs/notes, refs/changes, - // or other less-standard refs spaces are ignored since there - // may be a very large number of them). - if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { - if err := private.PushUpdate(models.PushUpdateOptions{ - RefFullName: refFullName, - OldCommitID: oldCommitID, - NewCommitID: newCommitID, - PusherID: pusherID, - PusherName: pusherName, - RepoUserName: repoUser, - RepoName: repoName, - }); err != nil { - log.GitLogger.Error("Update: %v", err) - } - } - - if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) { - branch := strings.TrimPrefix(refFullName, git.BranchPrefix) - repo, pullRequestAllowed, err := private.GetRepository(repoID) - if err != nil { - log.GitLogger.Error("get repo: %v", err) - break - } - if !pullRequestAllowed { - break - } - - baseRepo := repo - if repo.IsFork { - baseRepo = repo.BaseRepo - } - - if !repo.IsFork && branch == baseRepo.DefaultBranch { - break - } + res, err := private.HookPostReceive(repoUser, repoName, private.HookOptions{ + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + RefFullName: refFullName, + UserID: pusherID, + UserName: pusherName, + }) - pr, err := private.ActivePullRequest(baseRepo.ID, repo.ID, baseRepo.DefaultBranch, branch) - if err != nil { - log.GitLogger.Error("get active pr: %v", err) - break - } + if res == nil { + fail("Internal Server Error", err) + } - fmt.Fprintln(os.Stderr, "") - if pr == nil { - if repo.IsFork { - branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) - } - fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch) - fmt.Fprintf(os.Stderr, " %s/compare/%s...%s\n", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)) - } else { - fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") - fmt.Fprintf(os.Stderr, " %s/pulls/%d\n", baseRepo.HTMLURL(), pr.Index) - } - fmt.Fprintln(os.Stderr, "") + if res["message"] == false { + continue } + fmt.Fprintln(os.Stderr, "") + if res["create"] == true { + fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res["branch"]) + fmt.Fprintf(os.Stderr, " %s\n", res["url"]) + } else { + fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") + fmt.Fprintf(os.Stderr, " %s\n", res["url"]) + } + fmt.Fprintln(os.Stderr, "") } return nil diff --git a/cmd/serv.go b/cmd/serv.go index 393b026079633..f4d5778ba35ed 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -8,9 +8,11 @@ package cmd import ( "encoding/json" "fmt" + "net/http" + "net/url" "os" "os/exec" - "path/filepath" + "strconv" "strings" "time" @@ -68,7 +70,6 @@ func setup(logPath string) { log.DelLogger("console") setting.NewContext() checkLFSVersion() - log.NewGitLogger(filepath.Join(setting.LogRootPath, logPath)) } func parseCmd(cmd string) (string, string) { @@ -95,15 +96,14 @@ func fail(userMessage, logMessage string, args ...interface{}) { if !setting.ProdMode { fmt.Fprintf(os.Stderr, logMessage+"\n", args...) } - log.GitLogger.Fatal(logMessage, args...) return } - log.GitLogger.Close() os.Exit(1) } func runServ(c *cli.Context) error { + // FIXME: This needs to internationalised setup("serv.log") if setting.SSH.Disabled { @@ -116,9 +116,23 @@ func runServ(c *cli.Context) error { return nil } + keys := strings.Split(c.Args()[0], "-") + if len(keys) != 2 || keys[0] != "key" { + fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) + } + keyID := com.StrTo(keys[1]).MustInt64() + cmd := os.Getenv("SSH_ORIGINAL_COMMAND") if len(cmd) == 0 { - println("Hi there, You've successfully authenticated, but Gitea does not provide shell access.") + key, user, err := private.ServNoCommand(keyID) + if err != nil { + fail("Internal error", "Failed to check provided key: %v", err) + } + if key.Type == models.KeyTypeDeploy { + println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.") + } else { + println("Hi there: " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.") + } println("If this is unexpected, please log in with password and setup Gitea under another user.") return nil } @@ -152,41 +166,19 @@ func runServ(c *cli.Context) error { fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) } - stopCPUProfiler := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) + stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) + if err != nil { + fail("Internal Server Error", "Unable to start CPU profile: %v", err) + } defer func() { stopCPUProfiler() - pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) + err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) + if err != nil { + fail("Internal Server Error", "Unable to dump Mem Profile: %v", err) + } }() } - var ( - isWiki bool - unitType = models.UnitTypeCode - unitName = "code" - ) - if strings.HasSuffix(reponame, ".wiki") { - isWiki = true - unitType = models.UnitTypeWiki - unitName = "wiki" - reponame = reponame[:len(reponame)-5] - } - - os.Setenv(models.EnvRepoUsername, username) - if isWiki { - os.Setenv(models.EnvRepoIsWiki, "true") - } else { - os.Setenv(models.EnvRepoIsWiki, "false") - } - os.Setenv(models.EnvRepoName, reponame) - - repo, err := private.GetRepositoryByOwnerAndName(username, reponame) - if err != nil { - if strings.Contains(err.Error(), "Failed to get repository: repository does not exist") { - fail(accessDenied, "Repository does not exist: %s/%s", username, reponame) - } - fail("Internal error", "Failed to get repository: %v", err) - } - requestedMode, has := allowedCommands[verb] if !has { fail("Unknown git command", "Unknown git command %s", verb) @@ -202,97 +194,37 @@ func runServ(c *cli.Context) error { } } - // Prohibit push to mirror repositories. - if requestedMode > models.AccessModeRead && repo.IsMirror { - fail("mirror repository is read-only", "") - } - - // Allow anonymous clone for public repositories. - var ( - keyID int64 - user *models.User - ) - if requestedMode == models.AccessModeWrite || repo.IsPrivate || setting.Service.RequireSignInView { - keys := strings.Split(c.Args()[0], "-") - if len(keys) != 2 { - fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) - } - - key, err := private.GetPublicKeyByID(com.StrTo(keys[1]).MustInt64()) - if err != nil { - fail("Invalid key ID", "Invalid key ID[%s]: %v", c.Args()[0], err) - } - keyID = key.ID - - // Check deploy key or user key. - if key.Type == models.KeyTypeDeploy { - // Now we have to get the deploy key for this repo - deployKey, err := private.GetDeployKey(key.ID, repo.ID) - if err != nil { - fail("Key access denied", "Failed to access internal api: [key_id: %d, repo_id: %d]", key.ID, repo.ID) - } - - if deployKey == nil { - fail("Key access denied", "Deploy key access denied: [key_id: %d, repo_id: %d]", key.ID, repo.ID) - } - - if deployKey.Mode < requestedMode { - fail("Key permission denied", "Cannot push with read-only deployment key: %d to repo_id: %d", key.ID, repo.ID) - } - - // Update deploy key activity. - if err = private.UpdateDeployKeyUpdated(key.ID, repo.ID); err != nil { - fail("Internal error", "UpdateDeployKey: %v", err) - } - - // FIXME: Deploy keys aren't really the owner of the repo pushing changes - // however we don't have good way of representing deploy keys in hook.go - // so for now use the owner - os.Setenv(models.EnvPusherName, username) - os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", repo.OwnerID)) - } else { - user, err = private.GetUserByKeyID(key.ID) - if err != nil { - fail("internal error", "Failed to get user by key ID(%d): %v", keyID, err) - } - - if !user.IsActive || user.ProhibitLogin { - fail("Your account is not active or has been disabled by Administrator", - "User %s is disabled and have no access to repository %s", - user.Name, repoPath) - } - - mode, err := private.CheckUnitUser(user.ID, repo.ID, user.IsAdmin, unitType) - if err != nil { - fail("Internal error", "Failed to check access: %v", err) - } else if *mode < requestedMode { - clientMessage := accessDenied - if *mode >= models.AccessModeRead { - clientMessage = "You do not have sufficient authorization for this action" - } - fail(clientMessage, - "User %s does not have level %v access to repository %s's "+unitName, - user.Name, requestedMode, repoPath) + results, err := private.ServCommand(keyID, username, reponame, requestedMode, verb, lfsVerb) + if err != nil { + if private.IsErrServCommand(err) { + errServCommand := err.(private.ErrServCommand) + if errServCommand.StatusCode != http.StatusInternalServerError { + fail("Unauthorized", errServCommand.Error()) + } else { + fail("Internal Server Error", errServCommand.Error()) } - - os.Setenv(models.EnvPusherName, user.Name) - os.Setenv(models.EnvPusherID, fmt.Sprintf("%d", user.ID)) } + fail("Internal Server Error", err.Error()) } + os.Setenv(models.EnvRepoIsWiki, strconv.FormatBool(results.IsWiki)) + os.Setenv(models.EnvRepoName, results.RepoName) + os.Setenv(models.EnvRepoUsername, results.OwnerName) + os.Setenv(models.EnvPusherName, username) + os.Setenv(models.EnvPusherID, strconv.FormatInt(results.UserID, 10)) + os.Setenv(models.ProtectedBranchRepoID, strconv.FormatInt(results.RepoID, 10)) + os.Setenv(models.ProtectedBranchPRID, fmt.Sprintf("%d", 0)) //LFS token authentication if verb == lfsAuthenticateVerb { - url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, username, repo.Name) + url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) now := time.Now() claims := jwt.MapClaims{ - "repo": repo.ID, + "repo": results.RepoID, "op": lfsVerb, "exp": now.Add(setting.LFS.HTTPAuthExpiry).Unix(), "nbf": now.Unix(), - } - if user != nil { - claims["user"] = user.ID + "user": results.UserID, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -313,7 +245,6 @@ func runServ(c *cli.Context) error { if err != nil { fail("Internal error", "Failed to encode LFS json response: %v", err) } - return nil } @@ -329,14 +260,6 @@ func runServ(c *cli.Context) error { } else { gitcmd = exec.Command(verb, repoPath) } - if isWiki { - if err = private.InitWiki(repo.ID); err != nil { - fail("Internal error", "Failed to init wiki repo: %v", err) - } - } - - os.Setenv(models.ProtectedBranchRepoID, fmt.Sprintf("%d", repo.ID)) - os.Setenv(models.ProtectedBranchPRID, fmt.Sprintf("%d", 0)) gitcmd.Dir = setting.RepoRootPath gitcmd.Stdout = os.Stdout @@ -347,9 +270,9 @@ func runServ(c *cli.Context) error { } // Update user key activity. - if keyID > 0 { - if err = private.UpdatePublicKeyUpdated(keyID); err != nil { - fail("Internal error", "UpdatePublicKey: %v", err) + if results.KeyID > 0 { + if err = private.UpdatePublicKeyInRepo(results.KeyID, results.RepoID); err != nil { + fail("Internal error", "UpdatePublicKeyInRepo: %v", err) } } diff --git a/cmd/web.go b/cmd/web.go index 6da6ec942e8c8..e6d0300a15504 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -15,7 +15,6 @@ import ( "strings" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/routes" @@ -111,8 +110,6 @@ func runWeb(ctx *cli.Context) error { routers.GlobalInit() - external.RegisterParsers() - m := routes.NewMacaron() routes.RegisterRoutes(m) diff --git a/contrib/fhs-compliant-script/gitea b/contrib/fhs-compliant-script/gitea new file mode 100755 index 0000000000000..28ce651aabb95 --- /dev/null +++ b/contrib/fhs-compliant-script/gitea @@ -0,0 +1,42 @@ +#!/bin/bash + +######################################################################## +# This script some defaults for gitea to run in a FHS compliant manner # +######################################################################## + +# It assumes that you place this script as gitea in /usr/bin +# +# And place the original in /usr/lib/gitea with working files in /var/lib/gitea +# and main configuration in /etc/gitea/app.ini +GITEA="/usr/lib/gitea/gitea" +WORK_DIR="/var/lib/gitea" +APP_INI="/etc/gitea/app.ini" + +APP_INI_SET="" +for i in "$@"; do + case "$i" in + "-c") + APP_INI_SET=1 + ;; + "-c="*) + APP_INI_SET=1 + ;; + "--config") + APP_INI_SET=1 + ;; + "--config="*) + APP_INI_SET=1 + ;; + *) + ;; + esac +done + +if [ -z "$APP_INI_SET" ]; then + CONF_ARG="-c \"$APP_INI\"" +fi + +# Provide FHS compliant defaults to +GITEA_WORK_DIR="${GITEA_WORK_DIR:-$WORK_DIR}" "$GITEA" $CONF_ARG "$@" + + diff --git a/contrib/pr/checkout.go b/contrib/pr/checkout.go index 7af27c2a9e6b2..880c029510c23 100644 --- a/contrib/pr/checkout.go +++ b/contrib/pr/checkout.go @@ -20,6 +20,7 @@ import ( "strconv" "time" + "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/routes" @@ -113,6 +114,7 @@ func runPR() { log.Printf("[PR] Setting up router\n") //routers.GlobalInit() external.RegisterParsers() + markup.Init() m := routes.NewMacaron() routes.RegisterRoutes(m) diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 3ce7268d9d386..a674984a2584e 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -260,6 +260,9 @@ PASSWD = ; For Postgres, either "disable" (default), "require", or "verify-full" ; For MySQL, either "false" (default), "true", or "skip-verify" SSL_MODE = disable +; For MySQL only, either "utf8" or "utf8mb4", default is "utf8". +; NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. +CHARSET = utf8 ; For "sqlite3" and "tidb", use an absolute path when you start gitea as service PATH = data/gitea.db ; For "sqlite3" only. Query timeout @@ -501,10 +504,18 @@ SESSION_LIFE_TIME = 86400 [picture] AVATAR_UPLOAD_PATH = data/avatars -; Max Width and Height of uploaded avatars. This is to limit the amount of RAM -; used when resizing the image. +REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars +; How Gitea deals with missing repository avatars +; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used +REPOSITORY_AVATAR_FALLBACK = none +REPOSITORY_AVATAR_FALLBACK_IMAGE = /img/repo_default.png +; Max Width and Height of uploaded avatars. +; This is to limit the amount of RAM used when resizing the image. AVATAR_MAX_WIDTH = 4096 AVATAR_MAX_HEIGHT = 3072 +; Maximum alloved file size for uploaded avatars. +; This is to limit the amount of RAM used when resizing the image. +AVATAR_MAX_FILE_SIZE = 1048576 ; Chinese users can choose "duoshuo" ; or a custom avatar source, like: http://cn.gravatar.com/avatar/ GRAVATAR_SOURCE = gravatar @@ -668,6 +679,8 @@ MAX_GIT_DIFF_FILES = 100 ; Arguments for command 'git gc', e.g. "--aggressive --auto" ; see more on http://git-scm.com/docs/git-gc/ GC_ARGS = +; If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1 +EnableAutoGitWireProtocol = true ; Operation timeout in seconds [git.timeout] diff --git a/docker/root/etc/templates/app.ini b/docker/root/etc/templates/app.ini index 589271b4a0e41..20cbb9053ce47 100644 --- a/docker/root/etc/templates/app.ini +++ b/docker/root/etc/templates/app.ini @@ -35,6 +35,7 @@ PROVIDER_CONFIG = /data/gitea/sessions [picture] AVATAR_UPLOAD_PATH = /data/gitea/avatars +REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars [attachment] PATH = /data/gitea/attachments diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 882f8a8a925cf..ecc196c86e136 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -15,8 +15,8 @@ menu: # Configuration Cheat Sheet -This is a cheat sheet for the Gitea configuration file. It contains most settings -that can configured as well as their default values. +This is a cheat sheet for the Gitea configuration file. It contains most of the settings +that can be configured as well as their default values. Any changes to the Gitea configuration file should be made in `custom/conf/app.ini` or any corresponding location. When installing from a distribution, this will @@ -160,6 +160,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `USER`: **root**: Database username. - `PASSWD`: **\**: Database user password. Use \`your password\` for quoting if you use special characters in the password. - `SSL_MODE`: **disable**: For PostgreSQL and MySQL only. +- `CHARSET`: **utf8**: For MySQL only, either "utf8" or "utf8mb4", default is "utf8". NOTICE: for "utf8mb4" you must use MySQL InnoDB > 5.6. Gitea is unable to check this. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. - `LOG_SQL`: **true**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. @@ -289,7 +290,16 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`. - `DISABLE_GRAVATAR`: **false**: Enable this to use local avatars only. - `ENABLE_FEDERATED_AVATAR`: **false**: Enable support for federated avatars (see [http://www.libravatar.org](http://www.libravatar.org)). -- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store local and cached files. +- `AVATAR_UPLOAD_PATH`: **data/avatars**: Path to store user avatar image files. +- `REPOSITORY_AVATAR_UPLOAD_PATH`: **data/repo-avatars**: Path to store repository avatar image files. +- `REPOSITORY_AVATAR_FALLBACK`: **none**: How Gitea deals with missing repository avatars + - none = no avatar will be displayed + - random = random avatar will be generated + - image = default image will be used (which is set in `REPOSITORY_AVATAR_DEFAULT_IMAGE`) +- `REPOSITORY_AVATAR_FALLBACK_IMAGE`: **/img/repo_default.png**: Image used as default repository avatar (if `REPOSITORY_AVATAR_FALLBACK` is set to image and none was uploaded) +- `AVATAR_MAX_WIDTH`: **4096**: Maximum avatar image width in pixels. +- `AVATAR_MAX_HEIGHT`: **3072**: Maximum avatar image height in pixels. +- `AVATAR_MAX_FILE_SIZE`: **1048576** (1Mb): Maximum avatar image file size in bytes. ## Attachment (`attachment`) @@ -395,6 +405,7 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false` - `MAX_GIT_DIFF_LINE_CHARACTERS`: **5000**: Max character count per line highlighted in diff view. - `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view. - `GC_ARGS`: **\**: Arguments for command `git gc`, e.g. `--aggressive --auto`. See more on http://git-scm.com/docs/git-gc/ +- `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: If use git wire protocol version 2 when git version >= 2.18, default is true, set to false when you always want git wire protocol version 1 ## Git - Timeout settings (`git.timeout`) - `DEFAUlT`: **360**: Git operations default timeout seconds. diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index 021233f2d2c32..b9a16dd844caa 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -78,7 +78,8 @@ menu: - `NAME`: 数据库名称。 - `USER`: 数据库用户名。 - `PASSWD`: 数据库用户密码。 -- `SSL_MODE`: PostgreSQL数据库是否启用SSL模式。 +- `SSL_MODE`: MySQL 或 PostgreSQL数据库是否启用SSL模式。 +- `CHARSET`: **utf8**: 仅当数据库为 MySQL 时有效, 可以为 "utf8" 或 "utf8mb4"。注意:如果使用 "utf8mb4",你的 MySQL InnoDB 版本必须在 5.6 以上。 - `PATH`: Tidb 或者 SQLite3 数据文件存放路径。 - `LOG_SQL`: **true**: 显示生成的SQL,默认为真。 @@ -209,6 +210,7 @@ menu: - `CLONE`: **300**: 内部仓库间克隆的超时时间,单位秒 - `PULL`: **300**: 内部仓库间拉取的超时时间,单位秒 - `GC`: **60**: git仓库GC的超时时间,单位秒 +- `ENABLE_AUTO_GIT_WIRE_PROTOCOL`: **true**: 是否根据 Git Wire Protocol协议支持情况自动切换版本,当 git 版本在 2.18 及以上时会自动切换到版本2。为 `false` 则不切换。 ## API (`api`) diff --git a/docs/content/doc/advanced/logging-documentation.en-us.md b/docs/content/doc/advanced/logging-documentation.en-us.md index df3578694346f..790e750084efd 100644 --- a/docs/content/doc/advanced/logging-documentation.en-us.md +++ b/docs/content/doc/advanced/logging-documentation.en-us.md @@ -27,7 +27,6 @@ log groups: * The Router logger * The Access logger * The XORM logger -* A logger called the `GitLogger` which is used during hooks. There is also the go log logger. @@ -180,21 +179,6 @@ which will not be inherited from the `[log]` or relevant * `EXPRESSION` will default to `""` * `PREFIX` will default to `""` -### The Hook and Serv "GitLoggers" - -These are less well defined loggers. Essentially these should only be -used within Gitea's subsystems and cannot be configured at present. - -They will write log files in: - -* `%(ROOT_PATH)/hooks/pre-receive.log` -* `%(ROOT_PATH)/hooks/update.log` -* `%(ROOT_PATH)/hooks/post-receive.log` -* `%(ROOT_PATH)/serv.log` -* `%(ROOT_PATH)/http.log` - -In the future these logs may be rationalised. - ## Log outputs Gitea provides 4 possible log outputs: diff --git a/docs/content/doc/installation/from-binary.en-us.md b/docs/content/doc/installation/from-binary.en-us.md index 7e795029db171..12e38e8096d52 100644 --- a/docs/content/doc/installation/from-binary.en-us.md +++ b/docs/content/doc/installation/from-binary.en-us.md @@ -143,6 +143,15 @@ bind: address already in use` Gitea needs to be started on another free port. Th is possible using `./gitea web -p $PORT`. It's possible another instance of Gitea is already running. +### Running Gitea on Raspbian + +As of v1.8, there is a problem with the arm7 version of Gitea and it doesn't run on Raspberry Pi and similar devices. + +It is therefore recommended to switch to the arm6 version which has been tested and shown to work on Raspberry Pi and similar devices. + + ### Git error after updating to a new version of Gitea If the binary file name has been changed during the update to a new version of Gitea, diff --git a/go.mod b/go.mod index d02765fb10f44..c6ebe16039338 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 + github.com/oliamb/cutter v0.2.2 github.com/philhofer/fwd v1.0.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/pquerna/otp v0.0.0-20160912161815-54653902c20e @@ -129,7 +130,7 @@ require ( gopkg.in/macaron.v1 v1.3.2 gopkg.in/redis.v2 v2.3.2 // indirect gopkg.in/src-d/go-billy.v4 v4.3.0 - gopkg.in/src-d/go-git.v4 v4.10.0 + gopkg.in/src-d/go-git.v4 v4.11.0 gopkg.in/testfixtures.v2 v2.5.0 mvdan.cc/xurls/v2 v2.0.0 strk.kbt.io/projects/go/libravatar v0.0.0-20160628055650-5eed7bff870a diff --git a/go.sum b/go.sum index 6b0a59d5b5138..81b3aae5d456e 100644 --- a/go.sum +++ b/go.sum @@ -244,6 +244,8 @@ github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc h1:z1PgdCCmYYVL0BoJT github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc/go.mod h1:np1wUFZ6tyoke22qDJZY40URn9Ae51gX7ljIWXN5TJs= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k= +github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -378,8 +380,8 @@ gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs= gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.10.0 h1:NWjTJTQnk8UpIGlssuefyDZ6JruEjo5s88vm88uASbw= -gopkg.in/src-d/go-git.v4 v4.10.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= +gopkg.in/src-d/go-git.v4 v4.11.0 h1:cJwWgJ0DXifrNrXM6RGN1Y2yR60Rr1zQ9Q5DX5S9qgU= +gopkg.in/src-d/go-git.v4 v4.11.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= gopkg.in/testfixtures.v2 v2.5.0 h1:N08B7l2GzFQenyYbzqthDnKAA+cmb17iAZhhFxr7JHw= diff --git a/integrations/api_admin_org_test.go b/integrations/api_admin_org_test.go new file mode 100644 index 0000000000000..546ed861c255a --- /dev/null +++ b/integrations/api_admin_org_test.go @@ -0,0 +1,86 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/http" + "net/url" + "strings" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestAPIAdminOrgCreate(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session) + + var org = api.CreateOrgOption{ + UserName: "user2_org", + FullName: "User2's organization", + Description: "This organization created by admin for user2", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "private", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org) + resp := session.MakeRequest(t, req, http.StatusCreated) + + var apiOrg api.Organization + DecodeJSON(t, resp, &apiOrg) + + assert.Equal(t, org.UserName, apiOrg.UserName) + assert.Equal(t, org.FullName, apiOrg.FullName) + assert.Equal(t, org.Description, apiOrg.Description) + assert.Equal(t, org.Website, apiOrg.Website) + assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) + + models.AssertExistsAndLoadBean(t, &models.User{ + Name: org.UserName, + LowerName: strings.ToLower(org.UserName), + FullName: org.FullName, + }) + }) +} + +func TestAPIAdminOrgCreateBadVisibility(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session) + + var org = api.CreateOrgOption{ + UserName: "user2_org", + FullName: "User2's organization", + Description: "This organization created by admin for user2", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "notvalid", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org) + session.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) +} + +func TestAPIAdminOrgCreateNotAdmin(t *testing.T) { + prepareTestEnv(t) + nonAdminUsername := "user2" + session := loginUser(t, nonAdminUsername) + token := getTokenForLoggedInUser(t, session) + var org = api.CreateOrgOption{ + UserName: "user2_org", + FullName: "User2's organization", + Description: "This organization created by admin for user2", + Website: "https://try.gitea.io", + Location: "Shanghai", + Visibility: "public", + } + req := NewRequestWithJSON(t, "POST", "/api/v1/admin/users/user2/orgs?token="+token, &org) + session.MakeRequest(t, req, http.StatusForbidden) +} diff --git a/integrations/api_helper_for_declarative_test.go b/integrations/api_helper_for_declarative_test.go index 943981ead2f04..85f0ab621f846 100644 --- a/integrations/api_helper_for_declarative_test.go +++ b/integrations/api_helper_for_declarative_test.go @@ -5,11 +5,14 @@ package integrations import ( + "encoding/json" "fmt" "io/ioutil" "net/http" "testing" + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -150,3 +153,42 @@ func doAPICreateDeployKey(ctx APITestContext, keyname, keyFile string, readOnly ctx.Session.MakeRequest(t, req, http.StatusCreated) } } + +func doAPICreatePullRequest(ctx APITestContext, owner, repo, baseBranch, headBranch string) func(*testing.T) (api.PullRequest, error) { + return func(t *testing.T) (api.PullRequest, error) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", + owner, repo, ctx.Token) + req := NewRequestWithJSON(t, http.MethodPost, urlStr, &api.CreatePullRequestOption{ + Head: headBranch, + Base: baseBranch, + Title: fmt.Sprintf("create a pr from %s to %s", headBranch, baseBranch), + }) + + expected := 201 + if ctx.ExpectedCode != 0 { + expected = ctx.ExpectedCode + } + resp := ctx.Session.MakeRequest(t, req, expected) + decoder := json.NewDecoder(resp.Body) + pr := api.PullRequest{} + err := decoder.Decode(&pr) + return pr, err + } +} + +func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) func(*testing.T) { + return func(t *testing.T) { + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", + owner, repo, index, ctx.Token) + req := NewRequestWithJSON(t, http.MethodPost, urlStr, &auth.MergePullRequestForm{ + MergeMessageField: "doAPIMergePullRequest Merge", + Do: string(models.MergeStyleMerge), + }) + + if ctx.ExpectedCode != 0 { + ctx.Session.MakeRequest(t, req, ctx.ExpectedCode) + return + } + ctx.Session.MakeRequest(t, req, 200) + } +} diff --git a/integrations/api_org_test.go b/integrations/api_org_test.go index b36650f2e8b5f..34579aa1ea9c1 100644 --- a/integrations/api_org_test.go +++ b/integrations/api_org_test.go @@ -17,7 +17,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAPIOrg(t *testing.T) { +func TestAPIOrgCreate(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { session := loginUser(t, "user1") @@ -28,6 +28,7 @@ func TestAPIOrg(t *testing.T) { Description: "This organization created by user1", Website: "https://try.gitea.io", Location: "Shanghai", + Visibility: "limited", } req := NewRequestWithJSON(t, "POST", "/api/v1/orgs?token="+token, &org) resp := session.MakeRequest(t, req, http.StatusCreated) @@ -40,6 +41,7 @@ func TestAPIOrg(t *testing.T) { assert.Equal(t, org.Description, apiOrg.Description) assert.Equal(t, org.Website, apiOrg.Website) assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) models.AssertExistsAndLoadBean(t, &models.User{ Name: org.UserName, @@ -72,6 +74,50 @@ func TestAPIOrg(t *testing.T) { }) } +func TestAPIOrgEdit(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + + token := getTokenForLoggedInUser(t, session) + var org = api.EditOrgOption{ + FullName: "User3 organization new full name", + Description: "A new description", + Website: "https://try.gitea.io/new", + Location: "Beijing", + Visibility: "private", + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/user3?token="+token, &org) + resp := session.MakeRequest(t, req, http.StatusOK) + + var apiOrg api.Organization + DecodeJSON(t, resp, &apiOrg) + + assert.Equal(t, "user3", apiOrg.UserName) + assert.Equal(t, org.FullName, apiOrg.FullName) + assert.Equal(t, org.Description, apiOrg.Description) + assert.Equal(t, org.Website, apiOrg.Website) + assert.Equal(t, org.Location, apiOrg.Location) + assert.Equal(t, org.Visibility, apiOrg.Visibility) + }) +} + +func TestAPIOrgEditBadVisibility(t *testing.T) { + onGiteaRun(t, func(*testing.T, *url.URL) { + session := loginUser(t, "user1") + + token := getTokenForLoggedInUser(t, session) + var org = api.EditOrgOption{ + FullName: "User3 organization new full name", + Description: "A new description", + Website: "https://try.gitea.io/new", + Location: "Beijing", + Visibility: "badvisibility", + } + req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/user3?token="+token, &org) + session.MakeRequest(t, req, http.StatusUnprocessableEntity) + }) +} + func TestAPIOrgDeny(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { setting.Service.RequireSignInView = true diff --git a/integrations/api_repo_edit_test.go b/integrations/api_repo_edit_test.go new file mode 100644 index 0000000000000..3b2c916ab0c7b --- /dev/null +++ b/integrations/api_repo_edit_test.go @@ -0,0 +1,225 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +// getRepoEditOptionFromRepo gets the options for an existing repo exactly as is +func getRepoEditOptionFromRepo(repo *models.Repository) *api.EditRepoOption { + name := repo.Name + description := repo.Description + website := repo.Website + private := repo.IsPrivate + hasIssues := false + if _, err := repo.GetUnit(models.UnitTypeIssues); err == nil { + hasIssues = true + } + hasWiki := false + if _, err := repo.GetUnit(models.UnitTypeWiki); err == nil { + hasWiki = true + } + defaultBranch := repo.DefaultBranch + hasPullRequests := false + ignoreWhitespaceConflicts := false + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquash := false + if unit, err := repo.GetUnit(models.UnitTypePullRequests); err == nil { + config := unit.PullRequestsConfig() + hasPullRequests = true + ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts + allowMerge = config.AllowMerge + allowRebase = config.AllowRebase + allowRebaseMerge = config.AllowRebaseMerge + allowSquash = config.AllowSquash + } + archived := repo.IsArchived + return &api.EditRepoOption{ + Name: &name, + Description: &description, + Website: &website, + Private: &private, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + DefaultBranch: &defaultBranch, + HasPullRequests: &hasPullRequests, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquash, + Archived: &archived, + } +} + +// getNewRepoEditOption Gets the options to change everything about an existing repo by adding to strings or changing +// the boolean +func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption { + // Gives a new property to everything + name := *opts.Name + "renamed" + description := "new description" + website := "http://wwww.newwebsite.com" + private := !*opts.Private + hasIssues := !*opts.HasIssues + hasWiki := !*opts.HasWiki + defaultBranch := "master" + hasPullRequests := !*opts.HasPullRequests + ignoreWhitespaceConflicts := !*opts.IgnoreWhitespaceConflicts + allowMerge := !*opts.AllowMerge + allowRebase := !*opts.AllowRebase + allowRebaseMerge := !*opts.AllowRebaseMerge + allowSquash := !*opts.AllowSquash + archived := !*opts.Archived + + return &api.EditRepoOption{ + Name: &name, + Description: &description, + Website: &website, + Private: &private, + DefaultBranch: &defaultBranch, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + HasPullRequests: &hasPullRequests, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquash, + Archived: &archived, + } +} + +func TestAPIRepoEdit(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16 + user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org + user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos + repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo + repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo + repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo + + // Get user2's token + session := loginUser(t, user2.Name) + token2 := getTokenForLoggedInUser(t, session) + session = emptyTestSession(t) + // Get user4's token + session = loginUser(t, user4.Name) + token4 := getTokenForLoggedInUser(t, session) + session = emptyTestSession(t) + + // Test editing a repo1 which user2 owns, changing name and many properties + origRepoEditOption := getRepoEditOptionFromRepo(repo1) + repoEditOption := getNewRepoEditOption(origRepoEditOption) + url := fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token2) + req := NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp := session.MakeRequest(t, req, http.StatusOK) + var repo api.Repository + DecodeJSON(t, resp, &repo) + assert.NotNil(t, repo) + // check response + assert.Equal(t, *repoEditOption.Name, repo.Name) + assert.Equal(t, *repoEditOption.Description, repo.Description) + assert.Equal(t, *repoEditOption.Website, repo.Website) + assert.Equal(t, *repoEditOption.Archived, repo.Archived) + // check repo1 from database + repo1edited := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + repo1editedOption := getRepoEditOptionFromRepo(repo1edited) + assert.Equal(t, *repoEditOption.Name, *repo1editedOption.Name) + assert.Equal(t, *repoEditOption.Description, *repo1editedOption.Description) + assert.Equal(t, *repoEditOption.Website, *repo1editedOption.Website) + assert.Equal(t, *repoEditOption.Archived, *repo1editedOption.Archived) + assert.Equal(t, *repoEditOption.Private, *repo1editedOption.Private) + assert.Equal(t, *repoEditOption.HasWiki, *repo1editedOption.HasWiki) + // reset repo in db + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test editing a non-existing repo + name := "repodoesnotexist" + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{Name: &name}) + resp = session.MakeRequest(t, req, http.StatusNotFound) + + // Test editing repo16 by user4 who does not have write access + origRepoEditOption = getRepoEditOptionFromRepo(repo16) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token4) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusNotFound) + + // Tests a repo with no token given so will fail + origRepoEditOption = getRepoEditOptionFromRepo(repo16) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo16.Name) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusNotFound) + + // Test using access token for a private repo that the user of the token owns + origRepoEditOption = getRepoEditOptionFromRepo(repo16) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + // reset repo in db + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test making a repo public that is private + repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) + assert.True(t, repo16.IsPrivate) + private := false + repoEditOption = &api.EditRepoOption{ + Private: &private, + } + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) + assert.False(t, repo16.IsPrivate) + // Make it private again + private = true + repoEditOption.Private = &private + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test using org repo "user3/repo3" where user2 is a collaborator + origRepoEditOption = getRepoEditOptionFromRepo(repo3) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, repo3.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusOK) + // reset repo in db + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user3.Name, *repoEditOption.Name, token2) + req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Test using org repo "user3/repo3" with no user token + origRepoEditOption = getRepoEditOptionFromRepo(repo3) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s", user3.Name, repo3.Name) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusNotFound) + + // Test using repo "user2/repo1" where user4 is a NOT collaborator + origRepoEditOption = getRepoEditOptionFromRepo(repo1) + repoEditOption = getNewRepoEditOption(origRepoEditOption) + url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo1.Name, token4) + req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption) + session.MakeRequest(t, req, http.StatusForbidden) + }) +} diff --git a/integrations/api_repo_file_delete_test.go b/integrations/api_repo_file_delete_test.go index 57e2539e19182..e9029a669b977 100644 --- a/integrations/api_repo_file_delete_test.go +++ b/integrations/api_repo_file_delete_test.go @@ -108,7 +108,7 @@ func TestAPIDeleteFile(t *testing.T) { DecodeJSON(t, resp, &apiError) assert.Equal(t, expectedAPIError, apiError) - // Test creating a file in repo1 by user4 who does not have write access + // Test creating a file in repo16 by user4 who does not have write access fileID++ treePath = fmt.Sprintf("delete/file%d.txt", fileID) createFile(user2, repo16, treePath) diff --git a/integrations/api_user_orgs_test.go b/integrations/api_user_orgs_test.go index 63e67f4356a30..6611a429d1f68 100644 --- a/integrations/api_user_orgs_test.go +++ b/integrations/api_user_orgs_test.go @@ -38,6 +38,7 @@ func TestUserOrgs(t *testing.T) { Description: "", Website: "", Location: "", + Visibility: "public", }, }, orgs) } @@ -63,6 +64,7 @@ func TestMyOrgs(t *testing.T) { Description: "", Website: "", Location: "", + Visibility: "public", }, }, orgs) } diff --git a/integrations/git_helper_for_declarative_test.go b/integrations/git_helper_for_declarative_test.go index b4fead66253b0..235f4b4a9b74f 100644 --- a/integrations/git_helper_for_declarative_test.go +++ b/integrations/git_helper_for_declarative_test.go @@ -112,16 +112,44 @@ func doGitAddRemote(dstPath, remoteName string, u *url.URL) func(*testing.T) { } } -func doGitPushTestRepository(dstPath, remoteName, branch string) func(*testing.T) { +func doGitPushTestRepository(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { - _, err := git.NewCommand("push", "-u", remoteName, branch).RunInDir(dstPath) + _, err := git.NewCommand(append([]string{"push", "-u"}, args...)...).RunInDir(dstPath) assert.NoError(t, err) } } -func doGitPushTestRepositoryFail(dstPath, remoteName, branch string) func(*testing.T) { +func doGitPushTestRepositoryFail(dstPath string, args ...string) func(*testing.T) { return func(t *testing.T) { - _, err := git.NewCommand("push", "-u", remoteName, branch).RunInDir(dstPath) + _, err := git.NewCommand(append([]string{"push"}, args...)...).RunInDir(dstPath) assert.Error(t, err) } } + +func doGitCreateBranch(dstPath, branch string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand("checkout", "-b", branch).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitCheckoutBranch(dstPath string, args ...string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand(append([]string{"checkout"}, args...)...).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitMerge(dstPath string, args ...string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand(append([]string{"merge"}, args...)...).RunInDir(dstPath) + assert.NoError(t, err) + } +} + +func doGitPull(dstPath string, args ...string) func(*testing.T) { + return func(t *testing.T) { + _, err := git.NewCommand(append([]string{"pull"}, args...)...).RunInDir(dstPath) + assert.NoError(t, err) + } +} diff --git a/integrations/git_test.go b/integrations/git_test.go index ebbf04f9d084a..ce5aee493d835 100644 --- a/integrations/git_test.go +++ b/integrations/git_test.go @@ -13,11 +13,13 @@ import ( "os" "path" "path/filepath" + "strconv" "testing" "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -43,105 +45,23 @@ func testGit(t *testing.T, u *url.URL) { httpContext.Reponame = "repo-tmp-17" dstPath, err := ioutil.TempDir("", httpContext.Reponame) - var little, big, littleLFS, bigLFS string - assert.NoError(t, err) defer os.RemoveAll(dstPath) - t.Run("Standard", func(t *testing.T) { - PrintCurrentTest(t) - ensureAnonymousClone(t, u) - - t.Run("CreateRepo", doAPICreateRepository(httpContext, false)) - - u.Path = httpContext.GitPath() - u.User = url.UserPassword(username, userPassword) - - t.Run("Clone", doGitClone(dstPath, u)) - - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - little = commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - PrintCurrentTest(t) - big = commitAndPush(t, bigSize, dstPath) - }) - }) - }) - t.Run("LFS", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - //Setup git LFS - _, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath) - assert.NoError(t, err) - err = git.AddChanges(dstPath, false, ".gitattributes") - assert.NoError(t, err) - - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - littleLFS = commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - PrintCurrentTest(t) - bigLFS = commitAndPush(t, bigSize, dstPath) - }) - }) - t.Run("Locks", func(t *testing.T) { - PrintCurrentTest(t) - lockTest(t, u.String(), dstPath) - }) - }) - t.Run("Raw", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - - // Request raw paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", little)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", big)) - nilResp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, nilResp.Length) + t.Run("CreateRepo", doAPICreateRepository(httpContext, false)) + ensureAnonymousClone(t, u) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, littleSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - - }) - t.Run("Media", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") + u.Path = httpContext.GitPath() + u.User = url.UserPassword(username, userPassword) - // Request media paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", little)) - resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) + t.Run("Clone", doGitClone(dstPath, u)) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", big)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", littleLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Length) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-17/media/branch/master/", bigLFS)) - resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Length) - }) + little, big := standardCommitAndPushTest(t, dstPath) + littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath) + rawTest(t, &httpContext, little, big, littleLFS, bigLFS) + mediaTest(t, &httpContext, little, big, littleLFS, bigLFS) + t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) }) t.Run("SSH", func(t *testing.T) { PrintCurrentTest(t) @@ -151,109 +71,26 @@ func testGit(t *testing.T, u *url.URL) { //Setup key the user ssh key withKeyFile(t, keyname, func(keyFile string) { t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile)) - PrintCurrentTest(t) //Setup remote link + //TODO: get url from api sshURL := createSSHUrl(sshContext.GitPath(), u) //Setup clone folder dstPath, err := ioutil.TempDir("", sshContext.Reponame) assert.NoError(t, err) defer os.RemoveAll(dstPath) - var little, big, littleLFS, bigLFS string - - t.Run("Standard", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("CreateRepo", doAPICreateRepository(sshContext, false)) - - //TODO get url from api - t.Run("Clone", doGitClone(dstPath, sshURL)) - - //time.Sleep(5 * time.Minute) - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - little = commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - PrintCurrentTest(t) - big = commitAndPush(t, bigSize, dstPath) - }) - }) - }) - t.Run("LFS", func(t *testing.T) { - PrintCurrentTest(t) - t.Run("PushCommit", func(t *testing.T) { - PrintCurrentTest(t) - //Setup git LFS - _, err = git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("track", "data-file-*").RunInDir(dstPath) - assert.NoError(t, err) - err = git.AddChanges(dstPath, false, ".gitattributes") - assert.NoError(t, err) - - t.Run("Little", func(t *testing.T) { - PrintCurrentTest(t) - littleLFS = commitAndPush(t, littleSize, dstPath) - }) - t.Run("Big", func(t *testing.T) { - PrintCurrentTest(t) - bigLFS = commitAndPush(t, bigSize, dstPath) - }) - }) - t.Run("Locks", func(t *testing.T) { - PrintCurrentTest(t) - lockTest(t, u.String(), dstPath) - }) - }) - t.Run("Raw", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - - // Request raw paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", little)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", big)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) + t.Run("CreateRepo", doAPICreateRepository(sshContext, false)) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, littleSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + t.Run("Clone", doGitClone(dstPath, sshURL)) - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/raw/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.NotEqual(t, bigSize, resp.Body.Len()) - assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) - - }) - t.Run("Media", func(t *testing.T) { - PrintCurrentTest(t) - session := loginUser(t, "user2") - - // Request media paths - req := NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", little)) - resp := session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", big)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", littleLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, littleSize, resp.Body.Len()) - - req = NewRequest(t, "GET", path.Join("/user2/repo-tmp-18/media/branch/master/", bigLFS)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, bigSize, resp.Body.Len()) - }) + little, big := standardCommitAndPushTest(t, dstPath) + littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath) + rawTest(t, &sshContext, little, big, littleLFS, bigLFS) + mediaTest(t, &sshContext, little, big, littleLFS, bigLFS) + t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) }) }) @@ -267,35 +104,146 @@ func ensureAnonymousClone(t *testing.T, u *url.URL) { } -func lockTest(t *testing.T, remote, repoPath string) { - _, err := git.NewCommand("remote").AddArguments("set-url", "origin", remote).RunInDir(repoPath) //TODO add test ssh git-lfs-creds - assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath) +func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) { + t.Run("Standard", func(t *testing.T) { + PrintCurrentTest(t) + little, big = commitAndPushTest(t, dstPath, "data-file-") + }) + return +} + +func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) { + t.Run("LFS", func(t *testing.T) { + PrintCurrentTest(t) + prefix := "lfs-data-file-" + _, err := git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath) + assert.NoError(t, err) + _, err = git.NewCommand("lfs").AddArguments("track", prefix+"*").RunInDir(dstPath) + assert.NoError(t, err) + err = git.AddChanges(dstPath, false, ".gitattributes") + assert.NoError(t, err) + + littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix) + + t.Run("Locks", func(t *testing.T) { + PrintCurrentTest(t) + lockTest(t, dstPath) + }) + }) + return +} + +func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) { + t.Run("PushCommit", func(t *testing.T) { + PrintCurrentTest(t) + t.Run("Little", func(t *testing.T) { + PrintCurrentTest(t) + little = doCommitAndPush(t, littleSize, dstPath, prefix) + }) + t.Run("Big", func(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test in short mode.") + return + } + PrintCurrentTest(t) + big = doCommitAndPush(t, bigSize, dstPath, prefix) + }) + }) + return +} + +func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { + t.Run("Raw", func(t *testing.T) { + PrintCurrentTest(t) + username := ctx.Username + reponame := ctx.Reponame + + session := loginUser(t, username) + + // Request raw paths + req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little)) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Body.Len()) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, littleSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Body.Len()) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEqual(t, bigSize, resp.Body.Len()) + assert.Contains(t, resp.Body.String(), models.LFSMetaFileIdentifier) + } + }) +} + +func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) { + t.Run("Media", func(t *testing.T) { + PrintCurrentTest(t) + + username := ctx.Username + reponame := ctx.Reponame + + session := loginUser(t, username) + + // Request media paths + req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little)) + resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, littleSize, resp.Length) + + if !testing.Short() { + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + + req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS)) + resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK) + assert.Equal(t, bigSize, resp.Length) + } + }) +} + +func lockTest(t *testing.T, repoPath string) { + lockFileTest(t, "README.md", repoPath) +} + +func lockFileTest(t *testing.T, filename, repoPath string) { + _, err := git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath) assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("lock", "README.md").RunInDir(repoPath) + _, err = git.NewCommand("lfs").AddArguments("lock", filename).RunInDir(repoPath) assert.NoError(t, err) _, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath) assert.NoError(t, err) - _, err = git.NewCommand("lfs").AddArguments("unlock", "README.md").RunInDir(repoPath) + _, err = git.NewCommand("lfs").AddArguments("unlock", filename).RunInDir(repoPath) assert.NoError(t, err) } -func commitAndPush(t *testing.T, size int, repoPath string) string { - name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two") +func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string { + name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix) assert.NoError(t, err) - _, err = git.NewCommand("push").RunInDir(repoPath) //Push + _, err = git.NewCommand("push", "origin", "master").RunInDir(repoPath) //Push assert.NoError(t, err) return name } -func generateCommitWithNewData(size int, repoPath, email, fullName string) (string, error) { +func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) { //Generate random file data := make([]byte, size) _, err := rand.Read(data) if err != nil { return "", err } - tmpFile, err := ioutil.TempFile(repoPath, "data-file-") + tmpFile, err := ioutil.TempFile(repoPath, prefix) if err != nil { return "", err } @@ -325,3 +273,71 @@ func generateCommitWithNewData(size int, repoPath, email, fullName string) (stri }) return filepath.Base(tmpFile.Name()), err } + +func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { + return func(t *testing.T) { + PrintCurrentTest(t) + t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected")) + t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected")) + + ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame) + t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected")) + t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected")) + var pr api.PullRequest + var err error + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t) + assert.NoError(t, err) + }) + t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)) + t.Run("PullProtected", doGitPull(dstPath, "origin", "protected")) + t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username)) + + t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master")) + t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce")) + t.Run("GenerateCommit", func(t *testing.T) { + _, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") + assert.NoError(t, err) + }) + t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected")) + t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected")) + t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected")) + t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) + } +} + +func doProtectBranch(ctx APITestContext, branch string, userToWhitelist string) func(t *testing.T) { + // We are going to just use the owner to set the protection. + return func(t *testing.T) { + csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame))) + + if userToWhitelist == "" { + // Change branch to protected + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{ + "_csrf": csrf, + "protected": "on", + }) + ctx.Session.MakeRequest(t, req, http.StatusFound) + } else { + user, err := models.GetUserByName(userToWhitelist) + assert.NoError(t, err) + // Change branch to protected + req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{ + "_csrf": csrf, + "protected": "on", + "enable_whitelist": "on", + "whitelist_users": strconv.FormatInt(user.ID, 10), + }) + ctx.Session.MakeRequest(t, req, http.StatusFound) + } + // Check if master branch has been locked successfully + flashCookie := ctx.Session.GetCookie("macaron_flash") + assert.NotNil(t, flashCookie) + assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value) + } +} diff --git a/integrations/internal_test.go b/integrations/internal_test.go deleted file mode 100644 index ee0c0d18f157a..0000000000000 --- a/integrations/internal_test.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package integrations - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - - "github.com/stretchr/testify/assert" -) - -func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr, canPush bool) { - reqURL := fmt.Sprintf("/api/internal/branch/%d/%s", repoID, util.PathEscapeSegments(branchName)) - req := NewRequest(t, "GET", reqURL) - t.Log(reqURL) - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)) - - resp := MakeRequest(t, req, NoExpectedStatus) - if isErr { - assert.EqualValues(t, http.StatusInternalServerError, resp.Code) - } else { - assert.EqualValues(t, http.StatusOK, resp.Code) - var branch models.ProtectedBranch - t.Log(resp.Body.String()) - assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &branch)) - assert.Equal(t, canPush, !branch.IsProtected()) - } -} - -func TestInternal_GetProtectedBranch(t *testing.T) { - prepareTestEnv(t) - - assertProtectedBranch(t, 1, "master", false, true) - assertProtectedBranch(t, 1, "dev", false, true) - assertProtectedBranch(t, 1, "lunny/dev", false, true) -} diff --git a/integrations/links_test.go b/integrations/links_test.go index 84be7e05911bf..468c8a0f21d90 100644 --- a/integrations/links_test.go +++ b/integrations/links_test.go @@ -110,7 +110,7 @@ func testLinksAsUser(userName string, t *testing.T) { reqAPI := NewRequestf(t, "GET", "/api/v1/users/%s/repos", userName) respAPI := MakeRequest(t, reqAPI, http.StatusOK) - var apiRepos []api.Repository + var apiRepos []*api.Repository DecodeJSON(t, respAPI, &apiRepos) var repoLinks = []string{ diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go index 029c211a4b2ce..d32f41737a3e0 100644 --- a/models/issue_assignees_test.go +++ b/models/issue_assignees_test.go @@ -43,8 +43,7 @@ func TestUpdateAssignee(t *testing.T) { assert.NoError(t, err) var expectedAssignees []*User - expectedAssignees = append(expectedAssignees, user2) - expectedAssignees = append(expectedAssignees, user3) + expectedAssignees = append(expectedAssignees, user2, user3) for in, assignee := range assignees { assert.Equal(t, assignee.ID, expectedAssignees[in].ID) diff --git a/models/issue_mail.go b/models/issue_mail.go index 033c094c7562c..16f85ba37821f 100644 --- a/models/issue_mail.go +++ b/models/issue_mail.go @@ -16,7 +16,7 @@ import ( ) func (issue *Issue) mailSubject() string { - return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.Name, issue.Title, issue.Index) + return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index) } // mailIssueCommentToParticipants can be used for both new issue creation and comment. diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f3a090e41c17b..b95a74c3621db 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -227,6 +227,8 @@ var migrations = []Migration{ NewMigration("hash application token", hashAppToken), // v86 -> v87 NewMigration("add http method to webhook", addHTTPMethodToWebhook), + // v87 -> v88 + NewMigration("add avatar field to repository", addAvatarFieldToRepository), } // Migrate database to current version diff --git a/models/migrations/v64.go b/models/migrations/v64.go index 5bc7e36b516e7..e4a360f578986 100644 --- a/models/migrations/v64.go +++ b/models/migrations/v64.go @@ -83,7 +83,7 @@ func addMultipleAssignees(x *xorm.Engine) error { return err } - allIssues := []Issue{} + allIssues := []*Issue{} if err := sess.Find(&allIssues); err != nil { return err } @@ -104,7 +104,7 @@ func addMultipleAssignees(x *xorm.Engine) error { return err } - allAssignementComments := []Comment{} + allAssignementComments := []*Comment{} if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil { return err } diff --git a/models/migrations/v87.go b/models/migrations/v87.go new file mode 100644 index 0000000000000..94711ac669035 --- /dev/null +++ b/models/migrations/v87.go @@ -0,0 +1,18 @@ +// Copyright 2019 Gitea. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "github.com/go-xorm/xorm" +) + +func addAvatarFieldToRepository(x *xorm.Engine) error { + type Repository struct { + // ID(10-20)-md5(32) - must fit into 64 symbols + Avatar string `xorm:"VARCHAR(64)"` + } + + return x.Sync2(new(Repository)) +} diff --git a/models/models.go b/models/models.go index c7e58737ede83..c1d4c100d0027 100644 --- a/models/models.go +++ b/models/models.go @@ -14,6 +14,7 @@ import ( "path" "path/filepath" "strings" + "time" "code.gitea.io/gitea/modules/setting" @@ -59,8 +60,8 @@ var ( // DbCfg holds the database settings DbCfg struct { - Type, Host, Name, User, Passwd, Path, SSLMode string - Timeout int + Type, Host, Name, User, Passwd, Path, SSLMode, Charset string + Timeout int } // EnableSQLite3 use SQLite3 @@ -160,6 +161,7 @@ func LoadConfigs() { DbCfg.Passwd = sec.Key("PASSWD").String() } DbCfg.SSLMode = sec.Key("SSL_MODE").MustString("disable") + DbCfg.Charset = sec.Key("CHARSET").In("utf8", []string{"utf8", "utf8mb4"}) DbCfg.Path = sec.Key("PATH").MustString(filepath.Join(setting.AppDataPath, "gitea.db")) DbCfg.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500) } @@ -222,8 +224,8 @@ func getEngine() (*xorm.Engine, error) { if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL tls = "false" } - connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=utf8&parseTime=true&tls=%s", - DbCfg.User, DbCfg.Passwd, connType, DbCfg.Host, DbCfg.Name, Param, tls) + connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=%s&parseTime=true&tls=%s", + DbCfg.User, DbCfg.Passwd, connType, DbCfg.Host, DbCfg.Name, Param, DbCfg.Charset, tls) case "postgres": connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode) case "mssql": @@ -277,6 +279,11 @@ func SetEngine() (err error) { // so use log file to instead print to stdout. x.SetLogger(NewXORMLogger(setting.LogSQL)) x.ShowSQL(setting.LogSQL) + if DbCfg.Type == "mysql" { + x.SetMaxIdleConns(0) + x.SetConnMaxLifetime(3 * time.Second) + } + return nil } diff --git a/models/org.go b/models/org.go index b7db32ef1669a..6511072e2b7b0 100644 --- a/models/org.go +++ b/models/org.go @@ -162,8 +162,8 @@ func CreateOrganization(org, owner *User) (err error) { } // insert units for team - var units = make([]TeamUnit, 0, len(allRepUnitTypes)) - for _, tp := range allRepUnitTypes { + var units = make([]TeamUnit, 0, len(AllRepoUnitTypes)) + for _, tp := range AllRepoUnitTypes { units = append(units, TeamUnit{ OrgID: org.ID, TeamID: t.ID, diff --git a/models/repo.go b/models/repo.go index 3283223d5bbd4..d5eca3d22502e 100644 --- a/models/repo.go +++ b/models/repo.go @@ -7,9 +7,14 @@ package models import ( "bytes" + "crypto/md5" "errors" "fmt" "html/template" + + // Needed for jpeg support + _ "image/jpeg" + "image/png" "io/ioutil" "net/url" "os" @@ -21,6 +26,7 @@ import ( "strings" "time" + "code.gitea.io/gitea/modules/avatar" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -166,6 +172,9 @@ type Repository struct { CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` Topics []string `xorm:"TEXT JSON"` + // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols + Avatar string `xorm:"VARCHAR(64)"` + CreatedUnix util.TimeStamp `xorm:"INDEX created"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` } @@ -265,31 +274,64 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool) parent = repo.BaseRepo.innerAPIFormat(e, mode, true) } } + hasIssues := false + if _, err := repo.getUnit(e, UnitTypeIssues); err == nil { + hasIssues = true + } + hasWiki := false + if _, err := repo.getUnit(e, UnitTypeWiki); err == nil { + hasWiki = true + } + hasPullRequests := false + ignoreWhitespaceConflicts := false + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquash := false + if unit, err := repo.getUnit(e, UnitTypePullRequests); err == nil { + config := unit.PullRequestsConfig() + hasPullRequests = true + ignoreWhitespaceConflicts = config.IgnoreWhitespaceConflicts + allowMerge = config.AllowMerge + allowRebase = config.AllowRebase + allowRebaseMerge = config.AllowRebaseMerge + allowSquash = config.AllowSquash + } + return &api.Repository{ - ID: repo.ID, - Owner: repo.Owner.APIFormat(), - Name: repo.Name, - FullName: repo.FullName(), - Description: repo.Description, - Private: repo.IsPrivate, - Empty: repo.IsEmpty, - Archived: repo.IsArchived, - Size: int(repo.Size / 1024), - Fork: repo.IsFork, - Parent: parent, - Mirror: repo.IsMirror, - HTMLURL: repo.HTMLURL(), - SSHURL: cloneLink.SSH, - CloneURL: cloneLink.HTTPS, - Website: repo.Website, - Stars: repo.NumStars, - Forks: repo.NumForks, - Watchers: repo.NumWatches, - OpenIssues: repo.NumOpenIssues, - DefaultBranch: repo.DefaultBranch, - Created: repo.CreatedUnix.AsTime(), - Updated: repo.UpdatedUnix.AsTime(), - Permissions: permission, + ID: repo.ID, + Owner: repo.Owner.APIFormat(), + Name: repo.Name, + FullName: repo.FullName(), + Description: repo.Description, + Private: repo.IsPrivate, + Empty: repo.IsEmpty, + Archived: repo.IsArchived, + Size: int(repo.Size / 1024), + Fork: repo.IsFork, + Parent: parent, + Mirror: repo.IsMirror, + HTMLURL: repo.HTMLURL(), + SSHURL: cloneLink.SSH, + CloneURL: cloneLink.HTTPS, + Website: repo.Website, + Stars: repo.NumStars, + Forks: repo.NumForks, + Watchers: repo.NumWatches, + OpenIssues: repo.NumOpenIssues, + DefaultBranch: repo.DefaultBranch, + Created: repo.CreatedUnix.AsTime(), + Updated: repo.UpdatedUnix.AsTime(), + Permissions: permission, + HasIssues: hasIssues, + HasWiki: hasWiki, + HasPullRequests: hasPullRequests, + IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts, + AllowMerge: allowMerge, + AllowRebase: allowRebase, + AllowRebaseMerge: allowRebaseMerge, + AllowSquash: allowSquash, + AvatarURL: repo.AvatarLink(), } } @@ -336,10 +378,20 @@ func (repo *Repository) UnitEnabled(tp UnitType) bool { return false } -var ( - // ErrUnitNotExist organization does not exist - ErrUnitNotExist = errors.New("Unit does not exist") -) +// ErrUnitTypeNotExist represents a "UnitTypeNotExist" kind of error. +type ErrUnitTypeNotExist struct { + UT UnitType +} + +// IsErrUnitTypeNotExist checks if an error is a ErrUnitNotExist. +func IsErrUnitTypeNotExist(err error) bool { + _, ok := err.(ErrUnitTypeNotExist) + return ok +} + +func (err ErrUnitTypeNotExist) Error() string { + return fmt.Sprintf("Unit type does not exist: %s", err.UT.String()) +} // MustGetUnit always returns a RepoUnit object func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { @@ -363,6 +415,11 @@ func (repo *Repository) MustGetUnit(tp UnitType) *RepoUnit { Type: tp, Config: new(PullRequestsConfig), } + } else if tp == UnitTypeIssues { + return &RepoUnit{ + Type: tp, + Config: new(IssuesConfig), + } } return &RepoUnit{ Type: tp, @@ -384,7 +441,7 @@ func (repo *Repository) getUnit(e Engine, tp UnitType) (*RepoUnit, error) { return unit, nil } } - return nil, ErrUnitNotExist + return nil, ErrUnitTypeNotExist{tp} } func (repo *Repository) getOwner(e Engine) (err error) { @@ -1222,8 +1279,8 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err } // insert units for repo - var units = make([]RepoUnit, 0, len(defaultRepoUnits)) - for _, tp := range defaultRepoUnits { + var units = make([]RepoUnit, 0, len(DefaultRepoUnits)) + for _, tp := range DefaultRepoUnits { if tp == UnitTypeIssues { units = append(units, RepoUnit{ RepoID: repo.ID, @@ -1869,6 +1926,15 @@ func DeleteRepository(doer *User, uid, repoID int64) error { go HookQueue.Add(repo.ID) } + if len(repo.Avatar) > 0 { + avatarPath := repo.CustomAvatarPath() + if com.IsExist(avatarPath) { + if err := os.Remove(avatarPath); err != nil { + return fmt.Errorf("Failed to remove %s: %v", avatarPath, err) + } + } + } + DeleteRepoFromIndexer(repo) return nil } @@ -2452,3 +2518,179 @@ func (repo *Repository) GetUserFork(userID int64) (*Repository, error) { } return &forkedRepo, nil } + +// CustomAvatarPath returns repository custom avatar file path. +func (repo *Repository) CustomAvatarPath() string { + // Avatar empty by default + if len(repo.Avatar) <= 0 { + return "" + } + return filepath.Join(setting.RepositoryAvatarUploadPath, repo.Avatar) +} + +// GenerateRandomAvatar generates a random avatar for repository. +func (repo *Repository) GenerateRandomAvatar() error { + return repo.generateRandomAvatar(x) +} + +func (repo *Repository) generateRandomAvatar(e Engine) error { + idToString := fmt.Sprintf("%d", repo.ID) + + seed := idToString + img, err := avatar.RandomImage([]byte(seed)) + if err != nil { + return fmt.Errorf("RandomImage: %v", err) + } + + repo.Avatar = idToString + if err = os.MkdirAll(filepath.Dir(repo.CustomAvatarPath()), os.ModePerm); err != nil { + return fmt.Errorf("MkdirAll: %v", err) + } + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("Create: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, img); err != nil { + return fmt.Errorf("Encode: %v", err) + } + log.Info("New random avatar created for repository: %d", repo.ID) + + if _, err := e.ID(repo.ID).Cols("avatar").NoAutoTime().Update(repo); err != nil { + return err + } + + return nil +} + +// RemoveRandomAvatars removes the randomly generated avatars that were created for repositories +func RemoveRandomAvatars() error { + var ( + err error + ) + err = x. + Where("id > 0").BufferSize(setting.IterateBufferSize). + Iterate(new(Repository), + func(idx int, bean interface{}) error { + repository := bean.(*Repository) + stringifiedID := strconv.FormatInt(repository.ID, 10) + if repository.Avatar == stringifiedID { + return repository.DeleteAvatar() + } + return nil + }) + return err +} + +// RelAvatarLink returns a relative link to the repository's avatar. +func (repo *Repository) RelAvatarLink() string { + + // If no avatar - path is empty + avatarPath := repo.CustomAvatarPath() + if len(avatarPath) <= 0 || !com.IsFile(avatarPath) { + switch mode := setting.RepositoryAvatarFallback; mode { + case "image": + return setting.RepositoryAvatarFallbackImage + case "random": + if err := repo.GenerateRandomAvatar(); err != nil { + log.Error("GenerateRandomAvatar: %v", err) + } + default: + // default behaviour: do not display avatar + return "" + } + } + return setting.AppSubURL + "/repo-avatars/" + repo.Avatar +} + +// AvatarLink returns user avatar absolute link. +func (repo *Repository) AvatarLink() string { + link := repo.RelAvatarLink() + // link may be empty! + if len(link) > 0 { + if link[0] == '/' && link[1] != '/' { + return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL)[1:] + } + } + return link +} + +// UploadAvatar saves custom avatar for repository. +// FIXME: split uploads to different subdirs in case we have massive number of repos. +func (repo *Repository) UploadAvatar(data []byte) error { + m, err := avatar.Prepare(data) + if err != nil { + return err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + oldAvatarPath := repo.CustomAvatarPath() + + // Users can upload the same image to other repo - prefix it with ID + // Then repo will be removed - only it avatar file will be removed + repo.Avatar = fmt.Sprintf("%d-%x", repo.ID, md5.Sum(data)) + if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil { + return fmt.Errorf("UploadAvatar: Update repository avatar: %v", err) + } + + if err := os.MkdirAll(setting.RepositoryAvatarUploadPath, os.ModePerm); err != nil { + return fmt.Errorf("UploadAvatar: Failed to create dir %s: %v", setting.RepositoryAvatarUploadPath, err) + } + + fw, err := os.Create(repo.CustomAvatarPath()) + if err != nil { + return fmt.Errorf("UploadAvatar: Create file: %v", err) + } + defer fw.Close() + + if err = png.Encode(fw, *m); err != nil { + return fmt.Errorf("UploadAvatar: Encode png: %v", err) + } + + if len(oldAvatarPath) > 0 && oldAvatarPath != repo.CustomAvatarPath() { + if err := os.Remove(oldAvatarPath); err != nil { + return fmt.Errorf("UploadAvatar: Failed to remove old repo avatar %s: %v", oldAvatarPath, err) + } + } + + return sess.Commit() +} + +// DeleteAvatar deletes the repos's custom avatar. +func (repo *Repository) DeleteAvatar() error { + + // Avatar not exists + if len(repo.Avatar) == 0 { + return nil + } + + avatarPath := repo.CustomAvatarPath() + log.Trace("DeleteAvatar[%d]: %s", repo.ID, avatarPath) + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + repo.Avatar = "" + if _, err := sess.ID(repo.ID).Cols("avatar").Update(repo); err != nil { + return fmt.Errorf("DeleteAvatar: Update repository avatar: %v", err) + } + + if _, err := os.Stat(avatarPath); err == nil { + if err := os.Remove(avatarPath); err != nil { + return fmt.Errorf("DeleteAvatar: Failed to remove %s: %v", avatarPath, err) + } + } else { + // // Schrodinger: file may or may not exist. See err for details. + log.Trace("DeleteAvatar[%d]: %v", err) + } + return sess.Commit() +} diff --git a/models/repo_mirror.go b/models/repo_mirror.go index b58fa05dfe3b5..7579231d8cf52 100644 --- a/models/repo_mirror.go +++ b/models/repo_mirror.go @@ -20,6 +20,7 @@ import ( "github.com/Unknwon/com" "github.com/go-xorm/xorm" + "github.com/mcuadros/go-version" ) // MirrorQueue holds an UniqueQueue object of the mirror @@ -70,7 +71,17 @@ func (m *Mirror) ScheduleNextUpdate() { } func remoteAddress(repoPath string) (string, error) { - cmd := git.NewCommand("remote", "get-url", "origin") + var cmd *git.Command + binVersion, err := git.BinVersion() + if err != nil { + return "", err + } + if version.Compare(binVersion, "2.7", ">=") { + cmd = git.NewCommand("remote", "get-url", "origin") + } else { + cmd = git.NewCommand("config", "--get", "remote.origin.url") + } + result, err := cmd.RunInDir(repoPath) if err != nil { if strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { diff --git a/models/repo_test.go b/models/repo_test.go index eee3997868022..8411536d70e23 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -5,6 +5,11 @@ package models import ( + "bytes" + "crypto/md5" + "fmt" + "image" + "image/png" "testing" "code.gitea.io/gitea/modules/markup" @@ -158,3 +163,51 @@ func TestTransferOwnership(t *testing.T) { CheckConsistencyFor(t, &Repository{}, &User{}, &Team{}) } + +func TestUploadAvatar(t *testing.T) { + + // Generate image + myImage := image.NewRGBA(image.Rect(0, 0, 1, 1)) + var buff bytes.Buffer + png.Encode(&buff, myImage) + + assert.NoError(t, PrepareTestDatabase()) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + err := repo.UploadAvatar(buff.Bytes()) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf("%d-%x", 10, md5.Sum(buff.Bytes())), repo.Avatar) +} + +func TestUploadBigAvatar(t *testing.T) { + + // Generate BIG image + myImage := image.NewRGBA(image.Rect(0, 0, 5000, 1)) + var buff bytes.Buffer + png.Encode(&buff, myImage) + + assert.NoError(t, PrepareTestDatabase()) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + err := repo.UploadAvatar(buff.Bytes()) + assert.Error(t, err) +} + +func TestDeleteAvatar(t *testing.T) { + + // Generate image + myImage := image.NewRGBA(image.Rect(0, 0, 1, 1)) + var buff bytes.Buffer + png.Encode(&buff, myImage) + + assert.NoError(t, PrepareTestDatabase()) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) + + err := repo.UploadAvatar(buff.Bytes()) + assert.NoError(t, err) + + err = repo.DeleteAvatar() + assert.NoError(t, err) + + assert.Equal(t, "", repo.Avatar) +} diff --git a/models/unit.go b/models/unit.go index 697df696bc117..9f5c8d3cbbf60 100644 --- a/models/unit.go +++ b/models/unit.go @@ -58,8 +58,8 @@ func (u UnitType) ColorFormat(s fmt.State) { } var ( - // allRepUnitTypes contains all the unit types - allRepUnitTypes = []UnitType{ + // AllRepoUnitTypes contains all the unit types + AllRepoUnitTypes = []UnitType{ UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, @@ -69,8 +69,8 @@ var ( UnitTypeExternalTracker, } - // defaultRepoUnits contains the default unit types - defaultRepoUnits = []UnitType{ + // DefaultRepoUnits contains the default unit types + DefaultRepoUnits = []UnitType{ UnitTypeCode, UnitTypeIssues, UnitTypePullRequests, diff --git a/models/user.go b/models/user.go index 90ca189ef0e02..9ee27ddfbd449 100644 --- a/models/user.go +++ b/models/user.go @@ -6,7 +6,6 @@ package models import ( - "bytes" "container/list" "crypto/md5" "crypto/sha256" @@ -14,10 +13,7 @@ import ( "encoding/hex" "errors" "fmt" - "image" - - // Needed for jpeg support - _ "image/jpeg" + _ "image/jpeg" // Needed for jpeg support "image/png" "os" "path/filepath" @@ -39,7 +35,6 @@ import ( "github.com/go-xorm/builder" "github.com/go-xorm/core" "github.com/go-xorm/xorm" - "github.com/nfnt/resize" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/ssh" ) @@ -457,24 +452,11 @@ func (u *User) IsPasswordSet() bool { // UploadAvatar saves custom avatar for user. // FIXME: split uploads to different subdirs in case we have massive users. func (u *User) UploadAvatar(data []byte) error { - imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) - if err != nil { - return fmt.Errorf("DecodeConfig: %v", err) - } - if imgCfg.Width > setting.AvatarMaxWidth { - return fmt.Errorf("Image width is to large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth) - } - if imgCfg.Height > setting.AvatarMaxHeight { - return fmt.Errorf("Image height is to large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight) - } - - img, _, err := image.Decode(bytes.NewReader(data)) + m, err := avatar.Prepare(data) if err != nil { - return fmt.Errorf("Decode: %v", err) + return err } - m := resize.Resize(avatar.AvatarSize, avatar.AvatarSize, img, resize.NearestNeighbor) - sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { @@ -497,7 +479,7 @@ func (u *User) UploadAvatar(data []byte) error { } defer fw.Close() - if err = png.Encode(fw, m); err != nil { + if err = png.Encode(fw, *m); err != nil { return fmt.Errorf("Encode: %v", err) } @@ -849,10 +831,9 @@ func CreateUser(u *User) (err error) { return err } u.HashPassword(u.Passwd) - u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization + u.AllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization && !setting.Admin.DisableRegularOrgCreation u.MaxRepoCreation = -1 u.Theme = setting.UI.DefaultTheme - u.AllowCreateOrganization = !setting.Admin.DisableRegularOrgCreation if _, err = sess.Insert(u); err != nil { return err @@ -1639,7 +1620,7 @@ func SyncExternalUsers() { var sshKeysNeedUpdate bool // Find all users with this login type - var users []User + var users []*User x.Where("login_type = ?", LoginLDAP). And("login_source = ?", s.ID). Find(&users) @@ -1658,7 +1639,7 @@ func SyncExternalUsers() { // Search for existing user for _, du := range users { if du.LowerName == strings.ToLower(su.Username) { - usr = &du + usr = du break } } @@ -1741,7 +1722,7 @@ func SyncExternalUsers() { log.Trace("SyncExternalUsers[%s]: Deactivating user %s", s.Name, usr.Name) usr.IsActive = false - err = UpdateUserCols(&usr, "is_active") + err = UpdateUserCols(usr, "is_active") if err != nil { log.Error("SyncExternalUsers[%s]: Error deactivating user %s: %v", s.Name, usr.Name, err) } diff --git a/models/user_test.go b/models/user_test.go index f0a8dbdd47c71..6af9752c9b21e 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -261,6 +261,8 @@ func TestCreateUser_Issue5882(t *testing.T) { {&User{Name: "GiteaBot2", Email: "GiteaBot2@gitea.io", Passwd: passwd, MustChangePassword: false}, true}, } + setting.Service.DefaultAllowCreateOrganization = true + for _, v := range tt { setting.Admin.DisableRegularOrgCreation = v.disableOrgCreation diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index 38ee5415d954f..8b9e5877d9565 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -23,6 +23,7 @@ type InstallForm struct { DbPasswd string DbName string SSLMode string + Charset string `binding:"Required;In(utf8,utf8mb4)"` DbPath string AppName string `binding:"Required" locale:"install.app_name"` diff --git a/modules/avatar/avatar.go b/modules/avatar/avatar.go index f426978b3252a..cf3da6df5ed9d 100644 --- a/modules/avatar/avatar.go +++ b/modules/avatar/avatar.go @@ -5,13 +5,20 @@ package avatar import ( + "bytes" "fmt" "image" "image/color/palette" + // Enable PNG support: + _ "image/png" "math/rand" "time" + "code.gitea.io/gitea/modules/setting" + "github.com/issue9/identicon" + "github.com/nfnt/resize" + "github.com/oliamb/cutter" ) // AvatarSize returns avatar's size @@ -42,3 +49,46 @@ func RandomImageSize(size int, data []byte) (image.Image, error) { func RandomImage(data []byte) (image.Image, error) { return RandomImageSize(AvatarSize, data) } + +// Prepare accepts a byte slice as input, validates it contains an image of an +// acceptable format, and crops and resizes it appropriately. +func Prepare(data []byte) (*image.Image, error) { + imgCfg, _, err := image.DecodeConfig(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("DecodeConfig: %v", err) + } + if imgCfg.Width > setting.AvatarMaxWidth { + return nil, fmt.Errorf("Image width is too large: %d > %d", imgCfg.Width, setting.AvatarMaxWidth) + } + if imgCfg.Height > setting.AvatarMaxHeight { + return nil, fmt.Errorf("Image height is too large: %d > %d", imgCfg.Height, setting.AvatarMaxHeight) + } + + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("Decode: %v", err) + } + + if imgCfg.Width != imgCfg.Height { + var newSize, ax, ay int + if imgCfg.Width > imgCfg.Height { + newSize = imgCfg.Height + ax = (imgCfg.Width - imgCfg.Height) / 2 + } else { + newSize = imgCfg.Width + ay = (imgCfg.Height - imgCfg.Width) / 2 + } + + img, err = cutter.Crop(img, cutter.Config{ + Width: newSize, + Height: newSize, + Anchor: image.Point{ax, ay}, + }) + if err != nil { + return nil, err + } + } + + img = resize.Resize(AvatarSize, AvatarSize, img, resize.NearestNeighbor) + return &img, nil +} diff --git a/modules/avatar/avatar_test.go b/modules/avatar/avatar_test.go index 9eff5bc2be947..662d50faddb99 100644 --- a/modules/avatar/avatar_test.go +++ b/modules/avatar/avatar_test.go @@ -5,8 +5,11 @@ package avatar import ( + "io/ioutil" "testing" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" ) @@ -17,3 +20,49 @@ func Test_RandomImage(t *testing.T) { _, err = RandomImageSize(0, []byte("gogs@local")) assert.Error(t, err) } + +func Test_PrepareWithPNG(t *testing.T) { + setting.AvatarMaxWidth = 4096 + setting.AvatarMaxHeight = 4096 + + data, err := ioutil.ReadFile("testdata/avatar.png") + assert.NoError(t, err) + + imgPtr, err := Prepare(data) + assert.NoError(t, err) + + assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) + assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) +} + +func Test_PrepareWithJPEG(t *testing.T) { + setting.AvatarMaxWidth = 4096 + setting.AvatarMaxHeight = 4096 + + data, err := ioutil.ReadFile("testdata/avatar.jpeg") + assert.NoError(t, err) + + imgPtr, err := Prepare(data) + assert.NoError(t, err) + + assert.Equal(t, 290, (*imgPtr).Bounds().Max.X) + assert.Equal(t, 290, (*imgPtr).Bounds().Max.Y) +} + +func Test_PrepareWithInvalidImage(t *testing.T) { + setting.AvatarMaxWidth = 5 + setting.AvatarMaxHeight = 5 + + _, err := Prepare([]byte{}) + assert.EqualError(t, err, "DecodeConfig: image: unknown format") +} +func Test_PrepareWithInvalidImageSize(t *testing.T) { + setting.AvatarMaxWidth = 5 + setting.AvatarMaxHeight = 5 + + data, err := ioutil.ReadFile("testdata/avatar.png") + assert.NoError(t, err) + + _, err = Prepare(data) + assert.EqualError(t, err, "Image width is too large: 10 > 5") +} diff --git a/modules/avatar/testdata/avatar.jpeg b/modules/avatar/testdata/avatar.jpeg new file mode 100644 index 0000000000000..892b7baf78e4f Binary files /dev/null and b/modules/avatar/testdata/avatar.jpeg differ diff --git a/modules/avatar/testdata/avatar.png b/modules/avatar/testdata/avatar.png new file mode 100644 index 0000000000000..c0f7922961601 Binary files /dev/null and b/modules/avatar/testdata/avatar.png differ diff --git a/modules/base/tool.go b/modules/base/tool.go index 3a6e28a885e5d..dcf9155a07750 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -465,41 +465,41 @@ func Subtract(left interface{}, right interface{}) interface{} { var rleft, rright int64 var fleft, fright float64 var isInt = true - switch left := left.(type) { + switch v := left.(type) { case int: - rleft = int64(left) + rleft = int64(v) case int8: - rleft = int64(left) + rleft = int64(v) case int16: - rleft = int64(left) + rleft = int64(v) case int32: - rleft = int64(left) + rleft = int64(v) case int64: - rleft = left + rleft = v case float32: - fleft = float64(left) + fleft = float64(v) isInt = false case float64: - fleft = left + fleft = v isInt = false } - switch right := right.(type) { + switch v := right.(type) { case int: - rright = int64(right) + rright = int64(v) case int8: - rright = int64(right) + rright = int64(v) case int16: - rright = int64(right) + rright = int64(v) case int32: - rright = int64(right) + rright = int64(v) case int64: - rright = right + rright = v case float32: - fright = float64(right) + fright = float64(v) isInt = false case float64: - fright = right + fright = v isInt = false } diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index dcaf2fcbb01f3..ec9bc1eb52335 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -306,21 +306,21 @@ func TestFileSize(t *testing.T) { func TestSubtract(t *testing.T) { toFloat64 := func(n interface{}) float64 { - switch n := n.(type) { + switch v := n.(type) { case int: - return float64(n) + return float64(v) case int8: - return float64(n) + return float64(v) case int16: - return float64(n) + return float64(v) case int32: - return float64(n) + return float64(v) case int64: - return float64(n) + return float64(v) case float32: - return float64(n) + return float64(v) case float64: - return n + return v default: return 0.0 } diff --git a/modules/context/context.go b/modules/context/context.go index c7534a16cdcd1..1699d7aeccb7d 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -257,6 +257,13 @@ func Contexter() macaron.Handler { branchName = repo.DefaultBranch } prefix := setting.AppURL + path.Join(url.PathEscape(ownerName), url.PathEscape(repoName), "src", "branch", util.PathEscapeSegments(branchName)) + + appURL, _ := url.Parse(setting.AppURL) + + insecure := "" + if appURL.Scheme == string(setting.HTTP) { + insecure = "--insecure " + } c.Header().Set("Content-Type", "text/html") c.WriteHeader(http.StatusOK) c.Write([]byte(com.Expand(` @@ -266,7 +273,7 @@ func Contexter() macaron.Handler { - go get {GoGetImport} + go get {Insecure}{GoGetImport} `, map[string]string{ @@ -274,6 +281,7 @@ func Contexter() macaron.Handler { "CloneLink": models.ComposeHTTPSCloneURL(ownerName, repoName), "GoDocDirectory": prefix + "{/dir}", "GoDocFile": prefix + "{/dir}/{file}#L{line}", + "Insecure": insecure, }))) return } diff --git a/modules/context/repo.go b/modules/context/repo.go index f9ed9327ff976..0908340879cb0 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -188,7 +188,10 @@ func RetrieveBaseRepo(ctx *Context, repo *models.Repository) { // ComposeGoGetImport returns go-get-import meta content. func ComposeGoGetImport(owner, repo string) string { - return path.Join(setting.Domain, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) + /// setting.AppUrl is guaranteed to be parse as url + appURL, _ := url.Parse(setting.AppURL) + + return path.Join(appURL.Host, setting.AppSubURL, url.PathEscape(owner), url.PathEscape(repo)) } // EarlyResponseForGoGetMeta responses appropriate go-get meta with status 200 diff --git a/modules/git/notes.go b/modules/git/notes.go new file mode 100644 index 0000000000000..7aa5d89a79fd9 --- /dev/null +++ b/modules/git/notes.go @@ -0,0 +1,60 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "io/ioutil" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +// NotesRef is the git ref where Gitea will look for git-notes data. +// The value ("refs/notes/commits") is the default ref used by git-notes. +const NotesRef = "refs/notes/commits" + +// Note stores information about a note created using git-notes. +type Note struct { + Message []byte + Commit *Commit +} + +// GetNote retrieves the git-notes data for a given commit. +func GetNote(repo *Repository, commitID string, note *Note) error { + notes, err := repo.GetCommit(NotesRef) + if err != nil { + return err + } + + entry, err := notes.GetTreeEntryByPath(commitID) + if err != nil { + return err + } + + blob := entry.Blob() + dataRc, err := blob.DataAsync() + if err != nil { + return err + } + + defer dataRc.Close() + d, err := ioutil.ReadAll(dataRc) + if err != nil { + return err + } + note.Message = d + + commit, err := repo.gogitRepo.CommitObject(plumbing.Hash(notes.ID)) + if err != nil { + return err + } + + lastCommits, err := getLastCommitForPaths(commit, "", []string{commitID}) + if err != nil { + return err + } + note.Commit = convertCommit(lastCommits[commitID]) + + return nil +} diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go new file mode 100644 index 0000000000000..a954377f543d3 --- /dev/null +++ b/modules/git/notes_test.go @@ -0,0 +1,24 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetNotes(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := OpenRepository(bareRepo1Path) + assert.NoError(t, err) + + note := Note{} + err = GetNote(bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e) + assert.NoError(t, err) + assert.Equal(t, []byte("Note contents\n"), note.Message) + assert.Equal(t, "Vladimir Panteleev", note.Commit.Author.Name) +} diff --git a/modules/git/repo_ref_test.go b/modules/git/repo_ref_test.go index 2a3ea26a768e8..d32b34994c391 100644 --- a/modules/git/repo_ref_test.go +++ b/modules/git/repo_ref_test.go @@ -19,13 +19,14 @@ func TestRepository_GetRefs(t *testing.T) { refs, err := bareRepo1.GetRefs() assert.NoError(t, err) - assert.Len(t, refs, 4) + assert.Len(t, refs, 5) expectedRefs := []string{ BranchPrefix + "branch1", BranchPrefix + "branch2", BranchPrefix + "master", TagPrefix + "test", + NotesRef, } for _, ref := range refs { diff --git a/modules/git/tests/repos/repo1_bare/objects/28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 b/modules/git/tests/repos/repo1_bare/objects/28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 new file mode 100644 index 0000000000000..05dc4725eaa5c Binary files /dev/null and b/modules/git/tests/repos/repo1_bare/objects/28/345b214c5967bd9cdd98cc7f88f2f1ac574e02 differ diff --git a/modules/git/tests/repos/repo1_bare/objects/a4/79ead1abb694ffca26f67b09c8313b12fa2a13 b/modules/git/tests/repos/repo1_bare/objects/a4/79ead1abb694ffca26f67b09c8313b12fa2a13 new file mode 100644 index 0000000000000..35d27dcbe7a65 Binary files /dev/null and b/modules/git/tests/repos/repo1_bare/objects/a4/79ead1abb694ffca26f67b09c8313b12fa2a13 differ diff --git a/modules/git/tests/repos/repo1_bare/objects/ca/6b5ddf303169a72d2a2971acde4f6eea194e5c b/modules/git/tests/repos/repo1_bare/objects/ca/6b5ddf303169a72d2a2971acde4f6eea194e5c new file mode 100644 index 0000000000000..d4c2138b1595f --- /dev/null +++ b/modules/git/tests/repos/repo1_bare/objects/ca/6b5ddf303169a72d2a2971acde4f6eea194e5c @@ -0,0 +1,4 @@ +x��M +�0F]���B�&&m"�@\�Of�6�HG���� +~˷x��y���� ��?[����B�& +H 0 { - getContentHandler(ctx) - return - } - } else if ctx.Req.Method == "PUT" && ContentMatcher(ctx.Req) { + + getContentHandler(ctx) + return + } else if ctx.Req.Method == "PUT" { PutHandler(ctx) return } @@ -348,7 +346,7 @@ func VerifyHandler(ctx *context.Context) { return } - if !ContentMatcher(ctx.Req) { + if !MetaMatcher(ctx.Req) { writeStatus(ctx, 400) return } @@ -385,7 +383,6 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo } header := make(map[string]string) - header["Accept"] = contentMediaType if rv.Authorization == "" { //https://github.com/github/git-lfs/issues/1088 @@ -404,20 +401,20 @@ func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload boo if upload && !download { // Force client side verify action while gitea lacks proper server side verification - rep.Actions["verify"] = &link{Href: rv.VerifyLink(), Header: header} + verifyHeader := make(map[string]string) + for k, v := range header { + verifyHeader[k] = v + } + + // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662 + verifyHeader["Accept"] = metaMediaType + + rep.Actions["verify"] = &link{Href: rv.VerifyLink(), Header: verifyHeader} } return rep } -// ContentMatcher provides a mux.MatcherFunc that only allows requests that contain -// an Accept header with the contentMediaType -func ContentMatcher(r macaron.Request) bool { - mediaParts := strings.Split(r.Header.Get("Accept"), ";") - mt := mediaParts[0] - return mt == contentMediaType -} - // MetaMatcher provides a mux.MatcherFunc that only allows requests that contain // an Accept header with the metaMediaType func MetaMatcher(r macaron.Request) bool { diff --git a/modules/log/log.go b/modules/log/log.go index d18996d48d31d..8698e9eed3ae3 100644 --- a/modules/log/log.go +++ b/modules/log/log.go @@ -5,9 +5,7 @@ package log import ( - "fmt" "os" - "path" "runtime" "strings" ) @@ -17,9 +15,7 @@ var ( DEFAULT = "default" // NamedLoggers map of named loggers NamedLoggers = make(map[string]*Logger) - // GitLogger logger for git - GitLogger *Logger - prefix string + prefix string ) // NewLogger create a logger for the default logger @@ -72,19 +68,6 @@ func GetLogger(name string) *Logger { return NamedLoggers[DEFAULT] } -// NewGitLogger create a logger for git -// FIXME: use same log level as other loggers. -func NewGitLogger(logPath string) { - path := path.Dir(logPath) - - if err := os.MkdirAll(path, os.ModePerm); err != nil { - Fatal("Failed to create dir %s: %v", path, err) - } - - GitLogger = newLogger("git", 0) - GitLogger.SetLogger("file", "file", fmt.Sprintf(`{"level":"TRACE","filename":"%s","rotate":true,"maxsize":%d,"daily":true,"maxdays":7,"compress":true,"compressionLevel":-1, "stacktraceLevel":"NONE"}`, logPath, 1<<28)) -} - // GetLevel returns the minimum logger level func GetLevel() Level { return NamedLoggers[DEFAULT].GetLevel() diff --git a/modules/markup/markup.go b/modules/markup/markup.go index 0ea4099600e17..dc43b533c022a 100644 --- a/modules/markup/markup.go +++ b/modules/markup/markup.go @@ -15,6 +15,14 @@ import ( func Init() { getIssueFullPattern() NewSanitizer() + + // since setting maybe changed extensions, this will reload all parser extensions mapping + extParsers = make(map[string]Parser) + for _, parser := range parsers { + for _, ext := range parser.Extensions() { + extParsers[strings.ToLower(ext)] = parser + } + } } // Parser defines an interface for parsering markup file to HTML diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index 9a09fdac0ac10..f28d0b61e73be 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -11,9 +11,9 @@ type Downloader interface { GetMilestones() ([]*Milestone, error) GetReleases() ([]*Release, error) GetLabels() ([]*Label, error) - GetIssues(start, limit int) ([]*Issue, error) + GetIssues(page, perPage int) ([]*Issue, bool, error) GetComments(issueNumber int64) ([]*Comment, error) - GetPullRequests(start, limit int) ([]*PullRequest, error) + GetPullRequests(page, perPage int) ([]*PullRequest, error) } // DownloaderFactory defines an interface to match a downloader implementation and create a downloader diff --git a/modules/migrations/git.go b/modules/migrations/git.go index cbaa37282170a..335d44ec9b63a 100644 --- a/modules/migrations/git.go +++ b/modules/migrations/git.go @@ -53,9 +53,9 @@ func (g *PlainGitDownloader) GetReleases() ([]*base.Release, error) { return nil, ErrNotSupported } -// GetIssues returns issues according start and limit -func (g *PlainGitDownloader) GetIssues(start, limit int) ([]*base.Issue, error) { - return nil, ErrNotSupported +// GetIssues returns issues according page and perPage +func (g *PlainGitDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { + return nil, false, ErrNotSupported } // GetComments returns comments according issueNumber @@ -63,7 +63,7 @@ func (g *PlainGitDownloader) GetComments(issueNumber int64) ([]*base.Comment, er return nil, ErrNotSupported } -// GetPullRequests returns pull requests according start and limit +// GetPullRequests returns pull requests according page and perPage func (g *PlainGitDownloader) GetPullRequests(start, limit int) ([]*base.PullRequest, error) { return nil, ErrNotSupported } diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index dcffb360e3cff..4e930fa8318ab 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -68,10 +68,10 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, includeWiki bool) IsPrivate: repo.IsPrivate, Wiki: includeWiki, }) + g.repo = r if err != nil { return err } - g.repo = r g.gitRepo, err = git.OpenRepository(r.RepoPath()) return err } diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 8e1cd67df8321..21c1becedfb7d 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -272,71 +272,65 @@ func convertGithubReactions(reactions *github.Reactions) *base.Reactions { } // GetIssues returns issues according start and limit -func (g *GithubDownloaderV3) GetIssues(start, limit int) ([]*base.Issue, error) { - var perPage = 100 +func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { opt := &github.IssueListByRepoOptions{ Sort: "created", Direction: "asc", State: "all", ListOptions: github.ListOptions{ PerPage: perPage, + Page: page, }, } - var allIssues = make([]*base.Issue, 0, limit) - for { - issues, resp, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt) - if err != nil { - return nil, fmt.Errorf("error while listing repos: %v", err) - } - for _, issue := range issues { - if issue.IsPullRequest() { - continue - } - var body string - if issue.Body != nil { - body = *issue.Body - } - var milestone string - if issue.Milestone != nil { - milestone = *issue.Milestone.Title - } - var labels = make([]*base.Label, 0, len(issue.Labels)) - for _, l := range issue.Labels { - labels = append(labels, convertGithubLabel(&l)) - } - var reactions *base.Reactions - if issue.Reactions != nil { - reactions = convertGithubReactions(issue.Reactions) - } - var email string - if issue.User.Email != nil { - email = *issue.User.Email - } - allIssues = append(allIssues, &base.Issue{ - Title: *issue.Title, - Number: int64(*issue.Number), - PosterName: *issue.User.Login, - PosterEmail: email, - Content: body, - Milestone: milestone, - State: *issue.State, - Created: *issue.CreatedAt, - Labels: labels, - Reactions: reactions, - Closed: issue.ClosedAt, - IsLocked: *issue.Locked, - }) - if len(allIssues) >= limit { - return allIssues, nil - } + var allIssues = make([]*base.Issue, 0, perPage) + + issues, _, err := g.client.Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt) + if err != nil { + return nil, false, fmt.Errorf("error while listing repos: %v", err) + } + for _, issue := range issues { + if issue.IsPullRequest() { + continue } - if resp.NextPage == 0 { - break + var body string + if issue.Body != nil { + body = *issue.Body } - opt.Page = resp.NextPage + var milestone string + if issue.Milestone != nil { + milestone = *issue.Milestone.Title + } + var labels = make([]*base.Label, 0, len(issue.Labels)) + for _, l := range issue.Labels { + labels = append(labels, convertGithubLabel(&l)) + } + var reactions *base.Reactions + if issue.Reactions != nil { + reactions = convertGithubReactions(issue.Reactions) + } + + var email string + if issue.User.Email != nil { + email = *issue.User.Email + } + allIssues = append(allIssues, &base.Issue{ + Title: *issue.Title, + Number: int64(*issue.Number), + PosterName: *issue.User.Login, + PosterEmail: email, + Content: body, + Milestone: milestone, + State: *issue.State, + Created: *issue.CreatedAt, + Labels: labels, + Reactions: reactions, + Closed: issue.ClosedAt, + IsLocked: *issue.Locked, + }) } - return allIssues, nil + + return allIssues, len(issues) < perPage, nil } // GetComments returns comments according issueNumber @@ -379,97 +373,91 @@ func (g *GithubDownloaderV3) GetComments(issueNumber int64) ([]*base.Comment, er return allComments, nil } -// GetPullRequests returns pull requests according start and limit -func (g *GithubDownloaderV3) GetPullRequests(start, limit int) ([]*base.PullRequest, error) { +// GetPullRequests returns pull requests according page and perPage +func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, error) { opt := &github.PullRequestListOptions{ Sort: "created", Direction: "asc", State: "all", ListOptions: github.ListOptions{ - PerPage: 100, + PerPage: perPage, + Page: page, }, } - var allPRs = make([]*base.PullRequest, 0, 100) - for { - prs, resp, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt) - if err != nil { - return nil, fmt.Errorf("error while listing repos: %v", err) - } - for _, pr := range prs { - var body string - if pr.Body != nil { - body = *pr.Body - } - var milestone string - if pr.Milestone != nil { - milestone = *pr.Milestone.Title - } - var labels = make([]*base.Label, 0, len(pr.Labels)) - for _, l := range pr.Labels { - labels = append(labels, convertGithubLabel(l)) - } + var allPRs = make([]*base.PullRequest, 0, perPage) - // FIXME: This API missing reactions, we may need another extra request to get reactions + prs, _, err := g.client.PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt) + if err != nil { + return nil, fmt.Errorf("error while listing repos: %v", err) + } + for _, pr := range prs { + var body string + if pr.Body != nil { + body = *pr.Body + } + var milestone string + if pr.Milestone != nil { + milestone = *pr.Milestone.Title + } + var labels = make([]*base.Label, 0, len(pr.Labels)) + for _, l := range pr.Labels { + labels = append(labels, convertGithubLabel(l)) + } - var email string - if pr.User.Email != nil { - email = *pr.User.Email - } - var merged bool - // pr.Merged is not valid, so use MergedAt to test if it's merged - if pr.MergedAt != nil { - merged = true - } + // FIXME: This API missing reactions, we may need another extra request to get reactions - var headRepoName string - var cloneURL string - if pr.Head.Repo != nil { - headRepoName = *pr.Head.Repo.Name - cloneURL = *pr.Head.Repo.CloneURL - } - var mergeCommitSHA string - if pr.MergeCommitSHA != nil { - mergeCommitSHA = *pr.MergeCommitSHA - } + var email string + if pr.User.Email != nil { + email = *pr.User.Email + } + var merged bool + // pr.Merged is not valid, so use MergedAt to test if it's merged + if pr.MergedAt != nil { + merged = true + } - allPRs = append(allPRs, &base.PullRequest{ - Title: *pr.Title, - Number: int64(*pr.Number), - PosterName: *pr.User.Login, - PosterEmail: email, - Content: body, - Milestone: milestone, - State: *pr.State, - Created: *pr.CreatedAt, - Closed: pr.ClosedAt, - Labels: labels, - Merged: merged, - MergeCommitSHA: mergeCommitSHA, - MergedTime: pr.MergedAt, - IsLocked: pr.ActiveLockReason != nil, - Head: base.PullRequestBranch{ - Ref: *pr.Head.Ref, - SHA: *pr.Head.SHA, - RepoName: headRepoName, - OwnerName: *pr.Head.User.Login, - CloneURL: cloneURL, - }, - Base: base.PullRequestBranch{ - Ref: *pr.Base.Ref, - SHA: *pr.Base.SHA, - RepoName: *pr.Base.Repo.Name, - OwnerName: *pr.Base.User.Login, - }, - PatchURL: *pr.PatchURL, - }) - if len(allPRs) >= limit { - return allPRs, nil - } + var headRepoName string + var cloneURL string + if pr.Head.Repo != nil { + headRepoName = *pr.Head.Repo.Name + cloneURL = *pr.Head.Repo.CloneURL } - if resp.NextPage == 0 { - break + var mergeCommitSHA string + if pr.MergeCommitSHA != nil { + mergeCommitSHA = *pr.MergeCommitSHA } - opt.Page = resp.NextPage + + allPRs = append(allPRs, &base.PullRequest{ + Title: *pr.Title, + Number: int64(*pr.Number), + PosterName: *pr.User.Login, + PosterEmail: email, + Content: body, + Milestone: milestone, + State: *pr.State, + Created: *pr.CreatedAt, + Closed: pr.ClosedAt, + Labels: labels, + Merged: merged, + MergeCommitSHA: mergeCommitSHA, + MergedTime: pr.MergedAt, + IsLocked: pr.ActiveLockReason != nil, + Head: base.PullRequestBranch{ + Ref: *pr.Head.Ref, + SHA: *pr.Head.SHA, + RepoName: headRepoName, + OwnerName: *pr.Head.User.Login, + CloneURL: cloneURL, + }, + Base: base.PullRequestBranch{ + Ref: *pr.Base.Ref, + SHA: *pr.Base.SHA, + RepoName: *pr.Base.Repo.Name, + OwnerName: *pr.Base.User.Login, + }, + PatchURL: *pr.PatchURL, + }) } + return allPRs, nil } diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index e1d3efad588b8..c14292ecc6da0 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -166,9 +166,11 @@ func TestGitHubDownloadRepo(t *testing.T) { }, releases[len(releases)-1:]) // downloader.GetIssues() - issues, err := downloader.GetIssues(0, 3) + issues, isEnd, err := downloader.GetIssues(1, 8) assert.NoError(t, err) assert.EqualValues(t, 3, len(issues)) + assert.False(t, isEnd) + var ( closed1 = time.Date(2018, 10, 23, 02, 57, 43, 0, time.UTC) ) @@ -319,7 +321,7 @@ something like in the latest 15days could be enough don't you think ? }, comments[:3]) // downloader.GetPullRequests() - prs, err := downloader.GetPullRequests(0, 3) + prs, err := downloader.GetPullRequests(1, 3) assert.NoError(t, err) assert.EqualValues(t, 3, len(prs)) diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index ac55a2e7278a5..4b1229f9495c0 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -128,8 +128,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if opts.Issues { log.Trace("migrating issues and comments") - for { - issues, err := downloader.GetIssues(0, 100) + for i := 1; ; i++ { + issues, isEnd, err := downloader.GetIssues(i, 100) if err != nil { return err } @@ -160,7 +160,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } } - if len(issues) < 100 { + if isEnd { break } } @@ -168,8 +168,8 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if opts.PullRequests { log.Trace("migrating pull requests and comments") - for { - prs, err := downloader.GetPullRequests(0, 100) + for i := 1; ; i++ { + prs, err := downloader.GetPullRequests(i, 100) if err != nil { return err } diff --git a/modules/pprof/pprof.go b/modules/pprof/pprof.go index e02c2d0f2aba9..b63904e713626 100644 --- a/modules/pprof/pprof.go +++ b/modules/pprof/pprof.go @@ -9,34 +9,30 @@ import ( "io/ioutil" "runtime" "runtime/pprof" - - "code.gitea.io/gitea/modules/log" ) // DumpMemProfileForUsername dumps a memory profile at pprofDataPath as memprofile__ -func DumpMemProfileForUsername(pprofDataPath, username string) { +func DumpMemProfileForUsername(pprofDataPath, username string) error { f, err := ioutil.TempFile(pprofDataPath, fmt.Sprintf("memprofile_%s_", username)) if err != nil { - log.GitLogger.Fatal("Could not create memory profile: %v", err) + return err } defer f.Close() runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.GitLogger.Fatal("Could not write memory profile: %v", err) - } + return pprof.WriteHeapProfile(f) } // DumpCPUProfileForUsername dumps a CPU profile at pprofDataPath as cpuprofile__ // it returns the stop function which stops, writes and closes the CPU profile file -func DumpCPUProfileForUsername(pprofDataPath, username string) func() { +func DumpCPUProfileForUsername(pprofDataPath, username string) (func(), error) { f, err := ioutil.TempFile(pprofDataPath, fmt.Sprintf("cpuprofile_%s_", username)) if err != nil { - log.GitLogger.Fatal("Could not create cpu profile: %v", err) + return nil, err } pprof.StartCPUProfile(f) return func() { pprof.StopCPUProfile() f.Close() - } + }, nil } diff --git a/modules/private/branch.go b/modules/private/branch.go deleted file mode 100644 index db079f67eb016..0000000000000 --- a/modules/private/branch.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "encoding/json" - "fmt" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" -) - -// GetProtectedBranchBy get protected branch information -func GetProtectedBranchBy(repoID int64, branchName string) (*models.ProtectedBranch, error) { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/branch/%d/%s", repoID, util.PathEscapeSegments(branchName)) - log.GitLogger.Trace("GetProtectedBranchBy: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - - var branch models.ProtectedBranch - if err := json.NewDecoder(resp.Body).Decode(&branch); err != nil { - return nil, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return nil, fmt.Errorf("Failed to get protected branch: %s", decodeJSONError(resp).Err) - } - - return &branch, nil -} - -// CanUserPush returns if user can push -func CanUserPush(protectedBranchID, userID int64) (bool, error) { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/protectedbranch/%d/%d", protectedBranchID, userID) - log.GitLogger.Trace("CanUserPush: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return false, err - } - - var canPush = make(map[string]interface{}) - if err := json.NewDecoder(resp.Body).Decode(&canPush); err != nil { - return false, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return false, fmt.Errorf("Failed to retrieve push user: %s", decodeJSONError(resp).Err) - } - - return canPush["can_push"].(bool), nil -} - -// HasEnoughApprovals returns if true if pr has enough granted approvals. -func HasEnoughApprovals(protectedBranchID, prID int64) (bool, error) { - if prID <= 0 { - return false, nil - } - - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/protectedbranchpr/%d/%d", protectedBranchID, prID) - log.GitLogger.Trace("CanUserPush: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return false, err - } - - var canPush = make(map[string]interface{}) - if err := json.NewDecoder(resp.Body).Decode(&canPush); err != nil { - return false, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return false, fmt.Errorf("Failed to retrieve push user: %s", decodeJSONError(resp).Err) - } - - return canPush["can_push"].(bool), nil -} diff --git a/modules/private/hook.go b/modules/private/hook.go new file mode 100644 index 0000000000000..caa38195559f5 --- /dev/null +++ b/modules/private/hook.go @@ -0,0 +1,86 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package private + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "code.gitea.io/gitea/modules/setting" +) + +// Git environment variables +const ( + GitAlternativeObjectDirectories = "GIT_ALTERNATE_OBJECT_DIRECTORIES" + GitObjectDirectory = "GIT_OBJECT_DIRECTORY" + GitQuarantinePath = "GIT_QUARANTINE_PATH" +) + +// HookOptions represents the options for the Hook calls +type HookOptions struct { + OldCommitID string + NewCommitID string + RefFullName string + UserID int64 + UserName string + GitObjectDirectory string + GitAlternativeObjectDirectories string + ProtectedBranchID int64 +} + +// HookPreReceive check whether the provided commits are allowed +func HookPreReceive(ownerName, repoName string, opts HookOptions) (int, string) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&gitObjectDirectory=%s&gitAlternativeObjectDirectories=%s&prID=%d", + url.PathEscape(ownerName), + url.PathEscape(repoName), + url.QueryEscape(opts.OldCommitID), + url.QueryEscape(opts.NewCommitID), + url.QueryEscape(opts.RefFullName), + opts.UserID, + url.QueryEscape(opts.GitObjectDirectory), + url.QueryEscape(opts.GitAlternativeObjectDirectories), + opts.ProtectedBranchID, + ) + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return resp.StatusCode, decodeJSONError(resp).Err + } + + return http.StatusOK, "" +} + +// HookPostReceive updates services and users +func HookPostReceive(ownerName, repoName string, opts HookOptions) (map[string]interface{}, string) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s?old=%s&new=%s&ref=%s&userID=%d&username=%s", + url.PathEscape(ownerName), + url.PathEscape(repoName), + url.QueryEscape(opts.OldCommitID), + url.QueryEscape(opts.NewCommitID), + url.QueryEscape(opts.RefFullName), + opts.UserID, + url.QueryEscape(opts.UserName)) + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, decodeJSONError(resp).Err + } + res := map[string]interface{}{} + _ = json.NewDecoder(resp.Body).Decode(&res) + + return res, "" +} diff --git a/modules/private/internal.go b/modules/private/internal.go index 56852ce63c115..b4fee2680fbac 100644 --- a/modules/private/internal.go +++ b/modules/private/internal.go @@ -10,11 +10,8 @@ import ( "fmt" "net" "net/http" - "net/url" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/httplib" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) @@ -51,49 +48,3 @@ func newInternalRequest(url, method string) *httplib.Request { } return req } - -// CheckUnitUser check whether user could visit the unit of this repository -func CheckUnitUser(userID, repoID int64, isAdmin bool, unitType models.UnitType) (*models.AccessMode, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/user/%d/checkunituser?isAdmin=%t&unitType=%d", repoID, userID, isAdmin, unitType) - log.GitLogger.Trace("CheckUnitUser: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to CheckUnitUser: %s", decodeJSONError(resp).Err) - } - - var a models.AccessMode - if err := json.NewDecoder(resp.Body).Decode(&a); err != nil { - return nil, err - } - - return &a, nil -} - -// GetRepositoryByOwnerAndName returns the repository by given ownername and reponame. -func GetRepositoryByOwnerAndName(ownerName, repoName string) (*models.Repository, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repo/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) - log.GitLogger.Trace("GetRepositoryByOwnerAndName: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to get repository: %s", decodeJSONError(resp).Err) - } - - var repo models.Repository - if err := json.NewDecoder(resp.Body).Decode(&repo); err != nil { - return nil, err - } - - return &repo, nil -} diff --git a/modules/private/key.go b/modules/private/key.go index 1c6511846b771..ebc28eb871399 100644 --- a/modules/private/key.go +++ b/modules/private/key.go @@ -5,127 +5,15 @@ package private import ( - "encoding/json" "fmt" - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) -// UpdateDeployKeyUpdated update deploy key updates -func UpdateDeployKeyUpdated(keyID int64, repoID int64) error { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/keys/%d/update", repoID, keyID) - log.GitLogger.Trace("UpdateDeployKeyUpdated: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "POST").Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to update deploy key: %s", decodeJSONError(resp).Err) - } - return nil -} - -// GetDeployKey check if repo has deploy key -func GetDeployKey(keyID, repoID int64) (*models.DeployKey, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/keys/%d", repoID, keyID) - log.GitLogger.Trace("GetDeployKey: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - switch resp.StatusCode { - case 404: - return nil, nil - case 200: - var dKey models.DeployKey - if err := json.NewDecoder(resp.Body).Decode(&dKey); err != nil { - return nil, err - } - return &dKey, nil - default: - return nil, fmt.Errorf("Failed to get deploy key: %s", decodeJSONError(resp).Err) - } -} - -// HasDeployKey check if repo has deploy key -func HasDeployKey(keyID, repoID int64) (bool, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/has-keys/%d", repoID, keyID) - log.GitLogger.Trace("HasDeployKey: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return false, err - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - return true, nil - } - return false, nil -} - -// GetPublicKeyByID get public ssh key by his ID -func GetPublicKeyByID(keyID int64) (*models.PublicKey, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d", keyID) - log.GitLogger.Trace("GetPublicKeyByID: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to get repository: %s", decodeJSONError(resp).Err) - } - - var pKey models.PublicKey - if err := json.NewDecoder(resp.Body).Decode(&pKey); err != nil { - return nil, err - } - return &pKey, nil -} - -// GetUserByKeyID get user attached to key -func GetUserByKeyID(keyID int64) (*models.User, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/user", keyID) - log.GitLogger.Trace("GetUserByKeyID: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("Failed to get user: %s", decodeJSONError(resp).Err) - } - - var user models.User - if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { - return nil, err - } - - return &user, nil -} - -// UpdatePublicKeyUpdated update public key updates -func UpdatePublicKeyUpdated(keyID int64) error { +// UpdatePublicKeyInRepo update public key and if necessary deploy key updates +func UpdatePublicKeyInRepo(keyID, repoID int64) error { // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update", keyID) - log.GitLogger.Trace("UpdatePublicKeyUpdated: %s", reqURL) - + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) resp, err := newInternalRequest(reqURL, "POST").Response() if err != nil { return err diff --git a/modules/private/push_update.go b/modules/private/push_update.go deleted file mode 100644 index f3071b63ade1c..0000000000000 --- a/modules/private/push_update.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "encoding/json" - "fmt" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// PushUpdate update publick key updates -func PushUpdate(opt models.PushUpdateOptions) error { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + "api/internal/push/update" - log.GitLogger.Trace("PushUpdate: %s", reqURL) - - body, err := json.Marshal(&opt) - if err != nil { - return err - } - - resp, err := newInternalRequest(reqURL, "POST").Body(body).Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) - } - - return nil -} diff --git a/modules/private/repository.go b/modules/private/repository.go deleted file mode 100644 index cf8ae68409057..0000000000000 --- a/modules/private/repository.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "encoding/json" - "fmt" - "net/url" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// GetRepository return the repository by its ID and a bool about if it's allowed to have PR -func GetRepository(repoID int64) (*models.Repository, bool, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repository/%d", repoID) - log.GitLogger.Trace("GetRepository: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, false, err - } - - var repoInfo struct { - Repository *models.Repository - AllowPullRequest bool - } - if err := json.NewDecoder(resp.Body).Decode(&repoInfo); err != nil { - return nil, false, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return nil, false, fmt.Errorf("failed to retrieve repository: %s", decodeJSONError(resp).Err) - } - - return repoInfo.Repository, repoInfo.AllowPullRequest, nil -} - -// ActivePullRequest returns an active pull request if it exists -func ActivePullRequest(baseRepoID int64, headRepoID int64, baseBranch, headBranch string) (*models.PullRequest, error) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/active-pull-request?baseRepoID=%d&headRepoID=%d&baseBranch=%s&headBranch=%s", baseRepoID, headRepoID, url.QueryEscape(baseBranch), url.QueryEscape(headBranch)) - log.GitLogger.Trace("ActivePullRequest: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return nil, err - } - - var pr *models.PullRequest - if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil { - return nil, err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return nil, fmt.Errorf("failed to retrieve pull request: %s", decodeJSONError(resp).Err) - } - - return pr, nil -} diff --git a/modules/private/serv.go b/modules/private/serv.go new file mode 100644 index 0000000000000..5b4a27f11621a --- /dev/null +++ b/modules/private/serv.go @@ -0,0 +1,106 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package private + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/setting" +) + +// KeyAndOwner is the response from ServNoCommand +type KeyAndOwner struct { + Key *models.PublicKey `json:"key"` + Owner *models.User `json:"user"` +} + +// ServNoCommand returns information about the provided key +func ServNoCommand(keyID int64) (*models.PublicKey, *models.User, error) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", + keyID) + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("%s", decodeJSONError(resp).Err) + } + + var keyAndOwner KeyAndOwner + if err := json.NewDecoder(resp.Body).Decode(&keyAndOwner); err != nil { + return nil, nil, err + } + return keyAndOwner.Key, keyAndOwner.Owner, nil +} + +// ServCommandResults are the results of a call to the private route serv +type ServCommandResults struct { + IsWiki bool + IsDeployKey bool + KeyID int64 + KeyName string + UserName string + UserID int64 + OwnerName string + RepoName string + RepoID int64 +} + +// ErrServCommand is an error returned from ServCommmand. +type ErrServCommand struct { + Results ServCommandResults + Type string + Err string + StatusCode int +} + +func (err ErrServCommand) Error() string { + return err.Err +} + +// IsErrServCommand checks if an error is a ErrServCommand. +func IsErrServCommand(err error) bool { + _, ok := err.(ErrServCommand) + return ok +} + +// ServCommand preps for a serv call +func ServCommand(keyID int64, ownerName, repoName string, mode models.AccessMode, verbs ...string) (*ServCommandResults, error) { + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", + keyID, + url.PathEscape(ownerName), + url.PathEscape(repoName), + mode) + for _, verb := range verbs { + if verb != "" { + reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) + } + } + + resp, err := newInternalRequest(reqURL, "GET").Response() + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var errServCommand ErrServCommand + if err := json.NewDecoder(resp.Body).Decode(&errServCommand); err != nil { + return nil, err + } + errServCommand.StatusCode = resp.StatusCode + return nil, errServCommand + } + var results ServCommandResults + if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { + return nil, err + } + return &results, nil + +} diff --git a/modules/private/wiki.go b/modules/private/wiki.go deleted file mode 100644 index 4ad0cc7c4ef5c..0000000000000 --- a/modules/private/wiki.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "fmt" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -// InitWiki initwiki via repo id -func InitWiki(repoID int64) error { - // Ask for running deliver hook and test pull request tasks. - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/repositories/%d/wiki/init", repoID) - log.GitLogger.Trace("InitWiki: %s", reqURL) - - resp, err := newInternalRequest(reqURL, "GET").Response() - if err != nil { - return err - } - - defer resp.Body.Close() - - // All 2XX status codes are accepted and others will return an error - if resp.StatusCode/100 != 2 { - return fmt.Errorf("Failed to init wiki: %s", decodeJSONError(resp).Err) - } - - return nil -} diff --git a/modules/setting/git.go b/modules/setting/git.go index 8625c0e780401..673bff207e539 100644 --- a/modules/setting/git.go +++ b/modules/setting/git.go @@ -16,12 +16,13 @@ import ( var ( // Git settings Git = struct { - DisableDiffHighlight bool - MaxGitDiffLines int - MaxGitDiffLineCharacters int - MaxGitDiffFiles int - GCArgs []string `delim:" "` - Timeout struct { + DisableDiffHighlight bool + MaxGitDiffLines int + MaxGitDiffLineCharacters int + MaxGitDiffFiles int + GCArgs []string `delim:" "` + EnableAutoGitWireProtocol bool + Timeout struct { Default int Migrate int Mirror int @@ -30,11 +31,12 @@ var ( GC int `ini:"GC"` } `ini:"git.timeout"` }{ - DisableDiffHighlight: false, - MaxGitDiffLines: 1000, - MaxGitDiffLineCharacters: 5000, - MaxGitDiffFiles: 100, - GCArgs: []string{}, + DisableDiffHighlight: false, + MaxGitDiffLines: 1000, + MaxGitDiffLineCharacters: 5000, + MaxGitDiffFiles: 100, + GCArgs: []string{}, + EnableAutoGitWireProtocol: true, Timeout: struct { Default int Migrate int @@ -64,10 +66,19 @@ func newGit() { log.Fatal("Error retrieving git version: %v", err) } - log.Info("Git Version: %s", binVersion) - if version.Compare(binVersion, "2.9", ">=") { // Explicitly disable credential helper, otherwise Git credentials might leak git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "credential.helper=") } + + var format = "Git Version: %s" + var args = []interface{}{binVersion} + // Since git wire protocol has been released from git v2.18 + if Git.EnableAutoGitWireProtocol && version.Compare(binVersion, "2.18", ">=") { + git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "protocol.version=2") + format += ", Wire Protocol %s Enabled" + args = append(args, "Version 2") // for focus color + } + + log.Info(format, args...) } diff --git a/modules/setting/setting.go b/modules/setting/setting.go index de89c67d04dd9..ff53e9a3757f1 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -250,14 +250,18 @@ var ( } // Picture settings - AvatarUploadPath string - AvatarMaxWidth int - AvatarMaxHeight int - GravatarSource string - GravatarSourceURL *url.URL - DisableGravatar bool - EnableFederatedAvatar bool - LibravatarService *libravatar.Libravatar + AvatarUploadPath string + AvatarMaxWidth int + AvatarMaxHeight int + GravatarSource string + GravatarSourceURL *url.URL + DisableGravatar bool + EnableFederatedAvatar bool + LibravatarService *libravatar.Libravatar + AvatarMaxFileSize int64 + RepositoryAvatarUploadPath string + RepositoryAvatarFallback string + RepositoryAvatarFallbackImage string // Log settings LogLevel string @@ -835,8 +839,16 @@ func NewContext() { if !filepath.IsAbs(AvatarUploadPath) { AvatarUploadPath = path.Join(AppWorkPath, AvatarUploadPath) } + RepositoryAvatarUploadPath = sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "repo-avatars")) + forcePathSeparator(RepositoryAvatarUploadPath) + if !filepath.IsAbs(RepositoryAvatarUploadPath) { + RepositoryAvatarUploadPath = path.Join(AppWorkPath, RepositoryAvatarUploadPath) + } + RepositoryAvatarFallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") + RepositoryAvatarFallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString("/img/repo_default.png") AvatarMaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) AvatarMaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(3072) + AvatarMaxFileSize = sec.Key("AVATAR_MAX_FILE_SIZE").MustInt64(1048576) switch source := sec.Key("GRAVATAR_SOURCE").MustString("gravatar"); source { case "duoshuo": GravatarSource = "http://gravatar.duoshuo.com/avatar/" diff --git a/modules/structs/org.go b/modules/structs/org.go index fd15da1ce946c..08ab139975283 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -6,25 +6,27 @@ package structs // Organization represents an organization type Organization struct { - ID int64 `json:"id"` - UserName string `json:"username"` - FullName string `json:"full_name"` - AvatarURL string `json:"avatar_url"` - Description string `json:"description"` - Website string `json:"website"` - Location string `json:"location"` - Visibility VisibleType `json:"visibility"` + ID int64 `json:"id"` + UserName string `json:"username"` + FullName string `json:"full_name"` + AvatarURL string `json:"avatar_url"` + Description string `json:"description"` + Website string `json:"website"` + Location string `json:"location"` + Visibility string `json:"visibility"` } // CreateOrgOption options for creating an organization type CreateOrgOption struct { // required: true - UserName string `json:"username" binding:"Required"` - FullName string `json:"full_name"` - Description string `json:"description"` - Website string `json:"website"` - Location string `json:"location"` - Visibility VisibleType `json:"visibility"` + UserName string `json:"username" binding:"Required"` + FullName string `json:"full_name"` + Description string `json:"description"` + Website string `json:"website"` + Location string `json:"location"` + // possible values are `public` (default), `limited` or `private` + // enum: public,limited,private + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } // EditOrgOption options for editing an organization @@ -33,4 +35,7 @@ type EditOrgOption struct { Description string `json:"description"` Website string `json:"website"` Location string `json:"location"` + // possible values are `public`, `limited` or `private` + // enum: public,limited,private + Visibility string `json:"visibility" binding:"In(,public,limited,private)"` } diff --git a/modules/structs/org_type.go b/modules/structs/org_type.go index 86dc5c81cd6ab..4fb9b6fc0fdc8 100644 --- a/modules/structs/org_type.go +++ b/modules/structs/org_type.go @@ -40,6 +40,16 @@ func (vt VisibleType) IsPrivate() bool { return vt == VisibleTypePrivate } +// VisibilityString provides the mode string of the visibility type (public, limited, private) +func (vt VisibleType) String() string { + for k, v := range VisibilityModes { + if vt == v { + return k + } + } + return "" +} + // ExtractKeysFromMapString provides a slice of keys from map func ExtractKeysFromMapString(in map[string]VisibleType) (keys []string) { for k := range in { diff --git a/modules/structs/repo.go b/modules/structs/repo.go index b5283beeaa41b..b4d162b776fa9 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -41,8 +41,17 @@ type Repository struct { // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time - Updated time.Time `json:"updated_at"` - Permissions *Permission `json:"permissions,omitempty"` + Updated time.Time `json:"updated_at"` + Permissions *Permission `json:"permissions,omitempty"` + HasIssues bool `json:"has_issues"` + HasWiki bool `json:"has_wiki"` + HasPullRequests bool `json:"has_pull_requests"` + IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"` + AllowMerge bool `json:"allow_merge_commits"` + AllowRebase bool `json:"allow_rebase"` + AllowRebaseMerge bool `json:"allow_rebase_explicit"` + AllowSquash bool `json:"allow_squash_merge"` + AvatarURL string `json:"avatar_url"` } // CreateRepoOption options when creating repository @@ -70,38 +79,36 @@ type CreateRepoOption struct { // EditRepoOption options when editing a repository's properties // swagger:model type EditRepoOption struct { - // Name of the repository - // - // required: true + // name of the repository // unique: true - Name *string `json:"name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // A short description of the repository. + Name *string `json:"name,omitempty" binding:"OmitEmpty;AlphaDashDot;MaxSize(100);"` + // a short description of the repository. Description *string `json:"description,omitempty" binding:"MaxSize(255)"` - // A URL with more information about the repository. + // a URL with more information about the repository. Website *string `json:"website,omitempty" binding:"MaxSize(255)"` - // Either `true` to make the repository private or `false` to make it public. - // Note: You will get a 422 error if the organization restricts changing repository visibility to organization + // either `true` to make the repository private or `false` to make it public. + // Note: you will get a 422 error if the organization restricts changing repository visibility to organization // owners and a non-owner tries to change the value of private. Private *bool `json:"private,omitempty"` - // Either `true` to enable issues for this repository or `false` to disable them. - EnableIssues *bool `json:"enable_issues,omitempty"` - // Either `true` to enable the wiki for this repository or `false` to disable it. - EnableWiki *bool `json:"enable_wiki,omitempty"` - // Updates the default branch for this repository. + // either `true` to enable issues for this repository or `false` to disable them. + HasIssues *bool `json:"has_issues,omitempty"` + // either `true` to enable the wiki for this repository or `false` to disable it. + HasWiki *bool `json:"has_wiki,omitempty"` + // sets the default branch for this repository. DefaultBranch *string `json:"default_branch,omitempty"` - // Either `true` to allow pull requests, or `false` to prevent pull request. - EnablePullRequests *bool `json:"enable_pull_requests,omitempty"` - // Either `true` to ignore whitepace for conflicts, or `false` to not ignore whitespace. `enabled_pull_requests` must be `true`. - IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace,omitempty"` - // Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `enabled_pull_requests` must be `true`. + // either `true` to allow pull requests, or `false` to prevent pull request. + HasPullRequests *bool `json:"has_pull_requests,omitempty"` + // either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`. + IgnoreWhitespaceConflicts *bool `json:"ignore_whitespace_conflicts,omitempty"` + // either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`. AllowMerge *bool `json:"allow_merge_commits,omitempty"` - // Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `enabled_pull_requests` must be `true`. + // either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `has_pull_requests` must be `true`. AllowRebase *bool `json:"allow_rebase,omitempty"` - // Either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `enabled_pull_requests` must be `true`. + // either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `has_pull_requests` must be `true`. AllowRebaseMerge *bool `json:"allow_rebase_explicit,omitempty"` - // Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `enabled_pull_requests` must be `true`. - AllowSquashMerge *bool `json:"allow_squash_merge,omitempty"` - // `true` to archive this repository. Note: You cannot unarchive repositories through the API. + // either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`. + AllowSquash *bool `json:"allow_squash_merge,omitempty"` + // set to `true` to archive this repository. Archived *bool `json:"archived,omitempty"` } diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index ac8b9333fe57c..c447d267244c9 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -7,29 +7,43 @@ package structs // FileOptions options for all file APIs type FileOptions struct { - Message string `json:"message" binding:"Required"` - BranchName string `json:"branch"` - NewBranchName string `json:"new_branch"` - Author Identity `json:"author"` - Committer Identity `json:"committer"` + // message (optional) for the commit of this file. if not supplied, a default message will be used + Message string `json:"message" binding:"Required"` + // branch (optional) to base this file from. if not given, the default branch is used + BranchName string `json:"branch"` + // new_branch (optional) will make a new branch from `branch` before creating the file + NewBranchName string `json:"new_branch"` + // `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) + Author Identity `json:"author"` + Committer Identity `json:"committer"` } // CreateFileOptions options for creating files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type CreateFileOptions struct { FileOptions + // content must be base64 encoded + // required: true Content string `json:"content"` } // DeleteFileOptions options for deleting files (used for other File structs below) +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type DeleteFileOptions struct { FileOptions + // sha is the SHA for the file that already exists + // required: true SHA string `json:"sha" binding:"Required"` } // UpdateFileOptions options for updating files +// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type UpdateFileOptions struct { DeleteFileOptions - Content string `json:"content"` + // content must be base64 encoded + // required: true + Content string `json:"content"` + // from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL FromPath string `json:"from_path" binding:"MaxSize(500)"` } diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 24a383252b6c9..ef4a68add0c37 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -125,6 +125,7 @@ func NewFuncMap() []template.FuncMap { "RenderCommitMessage": RenderCommitMessage, "RenderCommitMessageLink": RenderCommitMessageLink, "RenderCommitBody": RenderCommitBody, + "RenderNote": RenderNote, "IsMultilineCommitMessage": IsMultilineCommitMessage, "ThemeColorMetaTag": func() string { return setting.UI.ThemeColorMetaTag @@ -155,8 +156,7 @@ func NewFuncMap() []template.FuncMap { var path []string index := strings.LastIndex(str, "/") if index != -1 && index != len(str) { - path = append(path, str[0:index+1]) - path = append(path, str[index+1:]) + path = append(path, str[0:index+1], str[index+1:]) } else { path = append(path, str) } @@ -329,10 +329,10 @@ func ToUTF8(content string) string { return res } -// ReplaceLeft replaces all prefixes 'old' in 's' with 'new'. -func ReplaceLeft(s, old, new string) string { - oldLen, newLen, i, n := len(old), len(new), 0, 0 - for ; i < len(s) && strings.HasPrefix(s[i:], old); n++ { +// ReplaceLeft replaces all prefixes 'oldS' in 's' with 'newS'. +func ReplaceLeft(s, oldS, newS string) string { + oldLen, newLen, i, n := len(oldS), len(newS), 0, 0 + for ; i < len(s) && strings.HasPrefix(s[i:], oldS); n++ { i += oldLen } @@ -347,7 +347,7 @@ func ReplaceLeft(s, old, new string) string { j := 0 for ; j < n*newLen; j += newLen { - copy(replacement[j:j+newLen], new) + copy(replacement[j:j+newLen], newS) } copy(replacement[j:], s[i:]) @@ -392,6 +392,17 @@ func RenderCommitBody(msg, urlPrefix string, metas map[string]string) template.H return template.HTML(strings.Join(body[1:], "\n")) } +// RenderNote renders the contents of a git-notes file as a commit message. +func RenderNote(msg, urlPrefix string, metas map[string]string) template.HTML { + cleanMsg := template.HTMLEscapeString(msg) + fullMessage, err := markup.RenderCommitMessage([]byte(cleanMsg), urlPrefix, "", metas) + if err != nil { + log.Error("RenderNote: %v", err) + return "" + } + return template.HTML(string(fullMessage)) +} + // IsMultilineCommitMessage checks to see if a commit message contains multiple lines. func IsMultilineCommitMessage(msg string) bool { return strings.Count(strings.TrimSpace(msg), "\n") >= 1 diff --git a/modules/util/url.go b/modules/util/url.go index 537e4c9b527c2..263255fcd3d6d 100644 --- a/modules/util/url.go +++ b/modules/util/url.go @@ -52,7 +52,8 @@ func IsExternalURL(rawURL string) bool { if err != nil { return true } - if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(setting.Domain, "www.", "", 1) { + appURL, _ := url.Parse(setting.AppURL) + if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(appURL.Host, "www.", "", 1) { return true } return false diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 3a2b4b71ffa98..2475065059e88 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -46,7 +46,7 @@ func TestURLJoin(t *testing.T) { } func TestIsExternalURL(t *testing.T) { - setting.Domain = "try.gitea.io" + setting.AppURL = "https://try.gitea.io" type test struct { Expected bool RawURL string diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 9a4dfab7a42d8..c22e667a2ebf9 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -7,6 +7,7 @@ package validation import ( "net" "net/url" + "regexp" "strings" "code.gitea.io/gitea/modules/setting" @@ -14,6 +15,8 @@ import ( var loopbackIPBlocks []*net.IPNet +var externalTrackerRegex = regexp.MustCompile(`({?)(?:user|repo|index)+?(}?)`) + func init() { for _, cidr := range []string{ "127.0.0.0/8", // IPv4 loopback @@ -75,3 +78,19 @@ func IsValidExternalURL(uri string) bool { return true } + +// IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers +func IsValidExternalTrackerURLFormat(uri string) bool { + if !IsValidExternalURL(uri) { + return false + } + + // check for typoed variables like /{index/ or /[repo} + for _, match := range externalTrackerRegex.FindAllStringSubmatch(uri, -1) { + if (match[1] == "{" || match[2] == "}") && (match[1] != "{" || match[2] != "}") { + return false + } + } + + return true +} diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 875625a02cdc0..9051ee1a0d19f 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -88,3 +88,70 @@ func Test_IsValidExternalURL(t *testing.T) { }) } } + +func Test_IsValidExternalTrackerURLFormat(t *testing.T) { + setting.AppURL = "https://try.gitea.io/" + + cases := []struct { + description string + url string + valid bool + }{ + { + description: "Correct external tracker URL with all placeholders", + url: "https://github.com/{user}/{repo}/issues/{index}", + valid: true, + }, + { + description: "Local external tracker URL with all placeholders", + url: "https://127.0.0.1/{user}/{repo}/issues/{index}", + valid: false, + }, + { + description: "External tracker URL with typo placeholder", + url: "https://github.com/{user}/{repo/issues/{index}", + valid: false, + }, + { + description: "External tracker URL with typo placeholder", + url: "https://github.com/[user}/{repo/issues/{index}", + valid: false, + }, + { + description: "External tracker URL with typo placeholder", + url: "https://github.com/{user}/repo}/issues/{index}", + valid: false, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/{user}/issues/{index}", + valid: true, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/{repo}/issues/{index}", + valid: true, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/issues/{index}", + valid: true, + }, + { + description: "External tracker URL missing optional placeholder", + url: "https://github.com/issues/{user}", + valid: true, + }, + { + description: "External tracker URL with similar placeholder names test", + url: "https://github.com/user/repo/issues/{index}", + valid: true, + }, + } + + for _, testCase := range cases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.valid, IsValidExternalTrackerURLFormat(testCase.url)) + }) + } +} diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 9c9740c1e8f0f..bd503ac5615d5 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -86,7 +86,6 @@ host=Hostitel user=Uživatelské jméno password=Heslo db_name=Název databáze -db_helper=Poznámka pro uživatele MySQL: použijte InnoDB engine a "utf8_general_ci" znakovou sadu. ssl_mode=SSL path=Cesta sqlite_helper=Cesta k souboru SQLite3 databáze.
Pokud spouštíte Gitea jako službu, zadejte absolutní cestu. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 5ab5dd39cff22..3a3e970d774a0 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -86,13 +86,18 @@ host=Host user=Benutzername password=Passwort db_name=Datenbankname -db_helper=Hinweis für MySQL-Benutzer: Bitte verwende das InnoDB-Speichersubsystem und den Zeichensatz „utf8_general_ci“. +db_helper=Hinweis für MySQL-Nutzer: Es sollte die InnoDB Storage Engine verwendet werden. Wird der "utf8mb4" Zeichensatz verwendet, so sollte die InnoDB Version neuer sein als 5.6 . ssl_mode=SSL +charset=Zeichensatz path=Pfad sqlite_helper=Dateipfad zur SQLite3 Datenbank.
Gebe einen absoluten Pfad an, wenn Gitea als Service gestartet wird. err_empty_db_path=Der SQLite3 Datenbankpfad darf nicht leer sein. no_admin_and_disable_registration=Du kannst Selbst-Registrierungen nicht deaktivieren, ohne ein Administratorkonto zu erstellen. err_empty_admin_password=Das Administrator-Passwort darf nicht leer sein. +err_empty_admin_email=Die Administrator-E-Mail darf nicht leer sein. +err_admin_name_is_reserved=Administratornutzername ist ungültig, der Nutzername ist reserviert +err_admin_name_pattern_not_allowed=Administratornutzername ist ungültig, der Nutzername entspricht einem verbotenem Muster +err_admin_name_is_invalid=Administratornutzername ist ungültig general_title=Allgemeine Einstellungen app_name=Seitentitel @@ -381,6 +386,7 @@ choose_new_avatar=Neues Profilbild auswählen update_avatar=Profilbild aktualisieren delete_current_avatar=Aktuelles Profilbild löschen uploaded_avatar_not_a_image=Die hochgeladene Datei ist kein Bild. +uploaded_avatar_is_too_big=Die hochgeladene Datei hat die maximale Größe überschritten. update_avatar_success=Dein Profilbild wurde geändert. change_password=Passwort aktualisieren @@ -596,6 +602,12 @@ form.name_pattern_not_allowed='%s' ist nicht erlaubt für Repository-Namen. need_auth=Authentifizierung zum Klonen benötigt migrate_type=Migrationstyp migrate_type_helper=Dieses Repository wird ein Mirror sein +migrate_items=Migrationselemente +migrate_items_wiki=Wiki +migrate_items_milestones=Meilensteine +migrate_items_labels=Labels +migrate_items_issues=Issues +migrate_items_pullrequests=Pull-Requests migrate_repo=Repository migrieren migrate.clone_address=Migrations- / Klon-URL migrate.clone_address_desc=Die HTTP(S)- oder „git clone“-URL eines bereits existierenden Repositorys @@ -604,6 +616,7 @@ migrate.permission_denied=Du hast keine Berechtigung zum Importieren lokaler Rep migrate.invalid_local_path=Der lokale Pfad ist ungültig, existiert nicht oder ist kein Ordner. migrate.failed=Fehler bei der Migration: %v migrate.lfs_mirror_unsupported=Spiegeln von LFS-Objekten wird nicht unterstützt - nutze stattdessen 'git lfs fetch --all' und 'git lfs push --all'. +migrate.migrate_items_options=Wenn du von GitHub migrierst und einen Benutzernamen eingegeben hast, werden die Migrationsoptionen angezeigt. mirror_from=Mirror von forked_from=geforkt von @@ -1258,7 +1271,9 @@ settings.protected_branch_can_push_yes=Du kannst pushen settings.protected_branch_can_push_no=Du kannst nicht pushen settings.branch_protection=Branch-Schutz für Branch „%s“ settings.protect_this_branch=Branch-Schutz aktivieren +settings.protect_this_branch_desc=Löschen verhindern und „force pushing” von Git auf dieser Branch deaktivieren. settings.protect_whitelist_committers=Push-Whitelist aktivieren +settings.protect_whitelist_committers_desc=Erlaube, dass Benutzer oder Teams, die auf der Whitelist stehen, auf diese Branch "pushen" dürfen (aber nicht "force pushen"). settings.protect_whitelist_users=Nutzer, die pushen dürfen: settings.protect_whitelist_search_users=Benutzer suchen… settings.protect_whitelist_teams=Teams, die pushen dürfen: @@ -1296,10 +1311,12 @@ settings.unarchive.header=Archivieren dieses Repos rückgängig machen settings.unarchive.text=Das Rückgängig machen dieses Repo-Archivs stellt seine Fähigkeit wieder her, Commits und Pushes anzunehmen, sowie neue Issues und Pull-Requests zu erstellen. settings.unarchive.success=Die Archivierung des Repos wurde erfolgreich wieder rückgängig gemacht. settings.unarchive.error=Beim Rückgängig machen dieses Repo-Archivs trat ein Fehler auf. In den Logs befinden sich mehr Details. +settings.update_avatar_success=Der Repository-Avatar wurde aktualisiert. diff.browse_source=Quellcode durchsuchen diff.parent=Ursprung diff.commit=Commit +diff.git-notes=Hinweise diff.data_not_available=Keine Diff-Daten verfügbar diff.show_diff_stats=Diff-Statistik anzeigen diff.show_split_view=Geteilte Ansicht @@ -1709,7 +1726,7 @@ config.db_path=Verzeichnis config.service_config=Service-Konfiguration config.register_email_confirm=E-Mail-Bestätigung benötigt zum Registrieren -config.disable_register=Selbstegistrierung deaktivieren +config.disable_register=Selbstregistrierung deaktivieren config.allow_only_external_registration=Registrierung nur über externe Services erlauben config.enable_openid_signup=OpenID-Selbstregistrierung aktivieren config.enable_openid_signin=OpenID-Anmeldung aktivieren diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a85221ab74b96..ebc6ca31ce30d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -86,13 +86,18 @@ host = Host user = Username password = Password db_name = Database Name -db_helper = Note to MySQL users: please use the InnoDB storage engine and the 'utf8_general_ci' character set. +db_helper = Note to MySQL users: please use the InnoDB storage engine and if you use "utf8mb4", your InnoDB version must be greater than 5.6 . ssl_mode = SSL +charset = Charset path = Path sqlite_helper = File path for the SQLite3 database.
Enter an absolute path if you run Gitea as a service. err_empty_db_path = The SQLite3 database path cannot be empty. no_admin_and_disable_registration = You cannot disable user self-registration without creating an administrator account. err_empty_admin_password = The administrator password cannot be empty. +err_empty_admin_email = The administrator email cannot be empty. +err_admin_name_is_reserved = Administrator Username is invalid, username is reserved +err_admin_name_pattern_not_allowed = Administrator Username is invalid, username is pattern is not allowed +err_admin_name_is_invalid = Administrator Username is invalid general_title = General Settings app_name = Site Title @@ -384,6 +389,7 @@ choose_new_avatar = Choose new avatar update_avatar = Update Avatar delete_current_avatar = Delete Current Avatar uploaded_avatar_not_a_image = The uploaded file is not an image. +uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size. update_avatar_success = Your avatar has been updated. change_password = Update Password @@ -1309,10 +1315,12 @@ settings.unarchive.header = Un-Archive This Repo settings.unarchive.text = Un-Archiving the repo will restore its ability to recieve commits and pushes, as well as new issues and pull-requests. settings.unarchive.success = The repo was successfully un-archived. settings.unarchive.error = An error occured while trying to un-archive the repo. See the log for more details. +settings.update_avatar_success = The repository avatar has been updated. diff.browse_source = Browse Source diff.parent = parent diff.commit = commit +diff.git-notes = Notes diff.data_not_available = Diff Content Not Available diff.show_diff_stats = Show Diff Stats diff.show_split_view = Split View @@ -1514,6 +1522,8 @@ dashboard.delete_repo_archives = Delete all repository archives dashboard.delete_repo_archives_success = All repository archives have been deleted. dashboard.delete_missing_repos = Delete all repositories missing their Git files dashboard.delete_missing_repos_success = All repositories missing their Git files have been deleted. +dashboard.delete_generated_repository_avatars = Delete generated repository avatars +dashboard.delete_generated_repository_avatars_success = Generated repository avatars were deleted. dashboard.git_gc_repos = Garbage collect all repositories dashboard.git_gc_repos_success = All repositories have finished garbage collection. dashboard.resync_all_sshkeys = Update the '.ssh/authorized_keys' file with Gitea SSH keys. (Not needed for the built-in SSH server.) diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 8fadb94a59b3a..4853680492bb4 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -70,7 +70,6 @@ host=Servidor user=Nombre de usuario password=Contraseña db_name=Nombre de la base de datos -db_helper=Nota para usuarios de la base de datos MySQL: por favor use el motor InnoDB y el esquema de caracteres 'utf8_general_ci'. ssl_mode=SSL path=Ruta diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index 72c20e448ec76..1537d49608008 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -70,7 +70,6 @@ host=Isäntä user=Käyttäjätunnus password=Salasana db_name=Tietokannan nimi -db_helper=Huomio MySQL-käyttäjille: käytäthän InnoDB-kantamoottoria ja 'utf8_general_ci'-merkistöä. ssl_mode=SSL path=Polku no_admin_and_disable_registration=Et voi kytkeä rekisteröintiä pois luomatta sitä ennen ylläpitotiliä. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index a6c57d2d04614..e6755f8cb5662 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -86,13 +86,18 @@ host=Hôte user=Nom d'utilisateur password=Mot de passe db_name=Nom de base de données -db_helper=Note aux utilisateurs de MySQL : veuillez utiliser le moteur de stockage InnoDB et le jeu de caractères 'utf8_general_ci'. +db_helper=Note aux utilisateurs de MySQL : utilisez le moteur de stockage InnoDB et si vous utilisez "utf8mb4", votre version InnoDB doit être supérieure à 5.6 . ssl_mode=SSL +charset=Jeu de caractères path=Emplacement sqlite_helper=Chemin d'accès pour la base de données SQLite3.
Entrer un chemin absolu si vous exécutez Gitea en tant que service. err_empty_db_path=Le chemin de la base de donnée SQLite3 ne peut être vide. no_admin_and_disable_registration=Vous ne pouvez pas désactiver la création de nouveaux utilisateurs avant d'avoir créé un compte administrateur. err_empty_admin_password=Le mot de passe administrateur ne peut pas être vide. +err_empty_admin_email=L'adresse e-mail de l'administrateur ne peut pas être vide. +err_admin_name_is_reserved=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est réservé +err_admin_name_pattern_not_allowed=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est interdit +err_admin_name_is_invalid=Le nom d'utilisateur de l'administrateur est invalide general_title=Configuration générale app_name=Titre du site @@ -300,6 +305,8 @@ password_not_match=Les mots de passe ne correspondent pas. username_been_taken=Le nom d'utilisateur est déjà pris. repo_name_been_taken=Ce nom de dépôt est déjà utilisé. +visit_rate_limit=Le taux d'appel à distance autorisé a été dépassé. +2fa_auth_required=L'accès à distance requiert une authentification à deux facteurs. org_name_been_taken=Ce nom d'organisation est déjà pris. team_name_been_taken=Le nom d'équipe est déjà pris. team_no_units_error=Autoriser l’accès à au moins une section du dépôt. @@ -381,6 +388,7 @@ choose_new_avatar=Sélectionner un nouvel avatar update_avatar=Mise à jour de l'avatar delete_current_avatar=Supprimer l'avatar actuel uploaded_avatar_not_a_image=Le fichier téléversé n'est pas une image. +uploaded_avatar_is_too_big=Le fichier téléversé dépasse la taille maximale. update_avatar_success=Votre avatar a été mis à jour. change_password=Modifier le mot de passe @@ -596,6 +604,13 @@ form.name_pattern_not_allowed=%s" n'est pas autorisé dans un nom de dépôt. need_auth=Autorisations de clonage migrate_type=Type de migration migrate_type_helper=Ce dépôt sera un miroir +migrate_items=Éléments à migrer +migrate_items_wiki=Wiki +migrate_items_milestones=Jalons +migrate_items_labels=Étiquettes +migrate_items_issues=Tickets +migrate_items_pullrequests=Demandes d'ajout +migrate_items_releases=Versions migrate_repo=Migrer le dépôt migrate.clone_address=Migrer/Cloner depuis une URL migrate.clone_address_desc=L'URL HTTP(S) ou Git "clone" d'un dépôt existant @@ -604,6 +619,7 @@ migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts lo migrate.invalid_local_path=Chemin local non valide, non existant ou n'étant pas un dossier. migrate.failed=Echec de migration: %v migrate.lfs_mirror_unsupported=La synchronisation des objets LFS n'est pas supportée - veuillez utiliser 'git lfs fetch --all' et 'git lfs push --all' à la place. +migrate.migrate_items_options=Quand vous migrez depuis github, une fois le nom d'utilisateur saisi, des options de migration supplémentaires seront affichées. mirror_from=miroir de forked_from=bifurqué depuis @@ -1258,7 +1274,9 @@ settings.protected_branch_can_push_yes=Vous pouvez pousser settings.protected_branch_can_push_no=Vous ne pouvez pas pousser settings.branch_protection=Protection de la branche "%s settings.protect_this_branch=Protection de la branche +settings.protect_this_branch_desc=Interdire de supprimer et de pousser sur la branche. settings.protect_whitelist_committers=Activer la liste blanche pour les mises à jour +settings.protect_whitelist_committers_desc=Autoriser les utilisateurs et les équipes sur liste blanche à supprimer et pousser sur la branche (mais pas de poussée forcée). settings.protect_whitelist_users=Utilisateurs en liste blanche : settings.protect_whitelist_search_users=Rechercher des utilisateurs… settings.protect_whitelist_teams=Équipes en liste blanche : @@ -1296,10 +1314,12 @@ settings.unarchive.header=Désarchiver ce dépôt settings.unarchive.text=Désarchiver le dépôt lui permettra de recevoir des révisions, ainsi que des nouveaux tickets ou demandes d'ajout. settings.unarchive.success=Ce dépôt a été désarchivé avec succès. settings.unarchive.error=Une erreur s'est produite durant le désarchivage. Referez-vous au journal pour plus de détails. +settings.update_avatar_success=L'avatar du dépôt a été mis à jour. diff.browse_source=Parcourir la source diff.parent=Parent diff.commit=révision +diff.git-notes=Notes diff.data_not_available=Contenu de la comparaison indisponible diff.show_diff_stats=Afficher les stats Diff diff.show_split_view=Vue séparée @@ -1754,6 +1774,7 @@ config.cache_config=Configuration du cache config.cache_adapter=Adaptateur du Cache config.cache_interval=Intervales du Cache config.cache_conn=Liaison du Cache +config.cache_item_ttl=Durée de vie des éléments dans le cache config.session_config=Configuration de session config.session_provider=Fournisseur de session diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 86a1ef8bfe85e..381d838834325 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -73,7 +73,6 @@ host=Host user=Nama Pengguna password=Kata Sandi db_name=Nama Basis Data -db_helper=Catatan untuk pengguna MySQL: Gunakan mesin penyimpanan InnoDB dan karakter set 'utf8_general_ci'. ssl_mode=SSL path=Jalur no_admin_and_disable_registration=Anda tidak dapat menonaktifkan pendaftaran tanpa membuat akun admin. diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 6028cb2c09cd6..a5e74f9b58242 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -73,7 +73,6 @@ host=Host user=Nome utente password=Password db_name=Nome del database -db_helper=Nota agli utenti MySQL: si prega di utilizza l'engine InnoDB ed il carattere di tipo 'utf8_general_ci'. ssl_mode=SSL path=Percorso no_admin_and_disable_registration=Non puoi disabilitare l'auto-registrazione degli utenti senza creare un account amministratore. diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index e87b2c9231858..e65162dfe2b71 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -86,13 +86,18 @@ host=ホスト user=ユーザー名 password=パスワード db_name=データベース名 -db_helper=MySQLユーザーへの注意: InnoDBストレージエンジンを使用し、キャラクターセットは 'utf8_general_ci' にしてください。 +db_helper=MySQLユーザーへの注意事項: InnoDBストレージエンジンを使用してください。 "utf8mb4"を使用する場合、InnoDBのバージョンは5.7以降にしてください。 ssl_mode=SSL +charset=文字セット path=パス sqlite_helper=SQLite3のデータベースファイルパス。
Giteaをサービスとして実行する場合は絶対パスを入力します。 err_empty_db_path=SQLite3のデータベースパスを空にすることはできません。 no_admin_and_disable_registration=管理者アカウントを作成せずに、セルフ登録を無効にすることはできません。 err_empty_admin_password=管理者パスワードは空にできません。 +err_empty_admin_email=管理者のメールアドレスは空にできません。 +err_admin_name_is_reserved=管理者のユーザー名が不正です。予約済みのユーザー名です。 +err_admin_name_pattern_not_allowed=管理者のユーザー名が不正です。使用できない形式のユーザー名です。 +err_admin_name_is_invalid=管理者のユーザー名が不正です general_title=基本設定 app_name=サイトタイトル @@ -383,6 +388,7 @@ choose_new_avatar=新しいアバターを選択 update_avatar=アバターを更新 delete_current_avatar=現在のアバターを削除 uploaded_avatar_not_a_image=アップロードしたファイルは画像ファイルではありません。 +uploaded_avatar_is_too_big=アップロードしたファイルは最大サイズを超えています。 update_avatar_success=アバターを更新しました。 change_password=パスワードを更新 @@ -1308,10 +1314,12 @@ settings.unarchive.header=このリポジトリをアーカイブ解除 settings.unarchive.text=リポジトリのアーカイブを解除すると、コミット、プッシュ、新規の課題やプルリクエストを受け付けるよう元に戻されます。 settings.unarchive.success=リポジトリのアーカイブを解除しました。 settings.unarchive.error=リポジトリのアーカイブ解除でエラーが発生しました。 詳細はログを確認してください。 +settings.update_avatar_success=リポジトリのアバターを更新しました。 diff.browse_source=ソースを参照 diff.parent=親 diff.commit=コミット +diff.git-notes=Notes diff.data_not_available=差分はありません diff.show_diff_stats=差分情報を表示 diff.show_split_view=分割表示 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index bb996bddb0620..87df76d789757 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -86,7 +86,6 @@ host=Resursdators user=Lietotāja vārds password=Parole db_name=Datu bāzes nosaukums -db_helper=MySQL lietotājiem: izmantojiet InnoDB dzini ar rakstzīmju kopu 'utf8_general_ci'. ssl_mode=SSL path=Ceļš sqlite_helper=Faila ceļš SQLite3 datu bāzei.
Ievadiet absolūto ceļu, ja Gitea tiek startēts kā serviss. diff --git a/options/locale/locale_nb-NO.ini b/options/locale/locale_nb-NO.ini index 2f0d8395f2856..5e316bf786ed5 100644 --- a/options/locale/locale_nb-NO.ini +++ b/options/locale/locale_nb-NO.ini @@ -86,7 +86,6 @@ host=Tjener user=Brukernavn password=Passord db_name=Databasenavn -db_helper=Merknad til MySQL brukere: Bruk InnoDB lagringsmotoren og 'utf8_general_ci' tegnsett. ssl_mode=SSL path=Bane sqlite_helper=Filbanen for SQLite3 databasen.
angi en absolutt bane hvis du kjører Gitea som en tjeneste. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index a78ec7b73c332..7f5fd504ae7aa 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -86,7 +86,6 @@ host=Server user=Gebruikersnaam password=Wachtwoord db_name=Database naam -db_helper=Opmerking voor MySQL-gebruikers: gebruik het InnoDB opslagsysteem en de "utf8_general_ci" tekenset. ssl_mode=SSL path=Pad sqlite_helper=Bestandspad voor de SQLite3-database.
Vul een volledig pad in als je GItea als een service uitvoert. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 84d866c844ae2..75fba79d02f38 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -84,7 +84,6 @@ host=Serwer user=Nazwa użytkownika password=Hasło db_name=Nazwa bazy danych -db_helper=Informacja dla użytkowników MySQL: używaj silnika bazy danych InnoDB oraz zestawu znaków "utf8_general_ci". ssl_mode=SSL path=Ścieżka no_admin_and_disable_registration=Nie możesz wyłączyć możliwości samodzielnej rejestracji kont użytkowników bez stworzenia konta administratora. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 66ada006d8449..65b26c6ee9101 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -86,13 +86,18 @@ host=Servidor user=Nome de usuário password=Senha db_name=Nome do banco de dados -db_helper=Nota para usuários do MySQL: por favor, use o mecanismo de armazenamento InnoDB e o conjunto de caracteres 'utf8_general_ci'. +db_helper=Informação para os usuários do MySQL: por favor use o mecanismo de armazenamento InnoDB e se você usar "utf8mb4", sua versão do InnoDB deve ser maior que 5.6. ssl_mode=SSL +charset=Charset path=Caminho sqlite_helper=Caminho do arquivo do banco de dados SQLite3.
Informe um caminho absoluto se você executar o Gitea como um serviço. err_empty_db_path=O caminho do banco de dados SQLite3 não pode ser em branco. no_admin_and_disable_registration=Você não pode desabilitar o auto-cadastro do usuário sem criar uma conta de administrador. err_empty_admin_password=A senha do administrador não pode ser em branco. +err_empty_admin_email=O e-mail do administrador não pode ser em branco. +err_admin_name_is_reserved=Nome de usuário do administrador é inválido, nome de usuário está reservado +err_admin_name_pattern_not_allowed=Nome de usuário do administrador é inválido, nome de usuário não é permitido +err_admin_name_is_invalid=Nome de usuário do administrador inválido general_title=Configurações gerais app_name=Nome do servidor @@ -383,6 +388,7 @@ choose_new_avatar=Escolha um novo avatar update_avatar=Atualizar o avatar delete_current_avatar=Excluir o avatar atual uploaded_avatar_not_a_image=O arquivo enviado não é uma imagem. +uploaded_avatar_is_too_big=O arquivo enviado excedeu o tamanho máximo. update_avatar_success=Seu avatar foi atualizado. change_password=Atualizar senha @@ -1308,10 +1314,12 @@ settings.unarchive.header=Desarquivar este repositório settings.unarchive.text=Desarquivando um repositório irá restaurar a capacidade do mesmo receber commits, pushs, assim como novas issues e pull requests. settings.unarchive.success=O repositório foi desarquivado com sucesso. settings.unarchive.error=Um erro ocorreu enquanto estava sendo desarquivado o repositório. Veja o log para mais detalhes. +settings.update_avatar_success=O avatar do repositório foi atualizado. diff.browse_source=Ver código fonte diff.parent=pai diff.commit=commit +diff.git-notes=Notas diff.data_not_available=Conteúdo de diff não disponível diff.show_diff_stats=Mostrar estatísticas do Diff diff.show_split_view=Visão dividida diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index d761e93bf44ac..b1e2508db512d 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -86,7 +86,6 @@ host=Хост user=Имя пользователя password=Пароль db_name=Имя базы данных -db_helper=Примечание для пользователей MySQL: пожалуйста, используйте хранилище InnoDB и набор символов 'utf8_general_ci'. ssl_mode=SSL path=Путь sqlite_helper=Путь к файлу базы данных SQLite3.
Введите абсолютный путь, если вы запускаете Gitea как службу. diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index a32854ccb5f24..d50e8b1c3ab35 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -82,7 +82,6 @@ host=Server user=Användarnamn password=Lösenord db_name=Databasens namn -db_helper=Notis för MySQL-användare: Använd InnoDB-lagringsmotorn och teckenuppsättning 'utf8_general_ci'. ssl_mode=SSL path=Filväg sqlite_helper=Sökväg för SQLite3-databasen.
Ange en absolut sökväg om du kör Gitea som en tjänst. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 648e5f66ef00b..6c5f45c5c47ec 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -86,7 +86,6 @@ host=Хост user=Ім'я кристувача password=Пароль db_name=Ім'я бази даних -db_helper=Примітка для користувачів MySQL: будь ласка, використовуйте InnoDB механізм зберігання і набір символів 'utf8_general_ci'. ssl_mode=SSL path=Шлях sqlite_helper=Шлях до файлу для бази даних SQLite3.
Введіть абсолютний шлях, якщо ви запускаєте Gіtea як сервіс. diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 92cac39779a67..5ac415460d721 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -86,7 +86,6 @@ host=数据库主机 user=用户名 password=数据库用户密码 db_name=数据库名称 -db_helper=MySQL 用户注意: 请使用 InnoDB 存储引擎和 "utf8_general_ci" 字符集。 ssl_mode=SSL path=数据库文件路径 sqlite_helper=SQLite3 数据库的文件路径。
如果以服务的方式运行 Gitea,请输入绝对路径。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index 87d3edaf94987..91e42a38573f1 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -74,7 +74,6 @@ host=主機 user=使用者名稱 password=密碼 db_name=資料庫名稱 -db_helper=MySQL 使用者注意: 請使用 InnoDB 儲存引擎和 "utf8_general_ci" 字元集。 ssl_mode=SSL path=資料庫文件路徑 no_admin_and_disable_registration=您不能夠在未建立管理員使用者的情況下禁止註冊。 diff --git a/public/css/index.css b/public/css/index.css index fa449ec69f8df..8950cc70386a2 100644 --- a/public/css/index.css +++ b/public/css/index.css @@ -803,6 +803,8 @@ footer .ui.left,footer .ui.right{line-height:40px} .stats-table .table-cell.tiny{height:.5em} tbody.commit-list{vertical-align:baseline} .commit-body{white-space:pre-wrap} +.git-notes.top{text-align:left} +.git-notes .commit-body{margin:0} @media only screen and (max-width:767px){.ui.stackable.menu.mobile--margin-between-items>.item{margin-top:5px;margin-bottom:5px} .ui.stackable.menu.mobile--no-negative-margins{margin-left:0;margin-right:0} } @@ -954,6 +956,7 @@ tbody.commit-list{vertical-align:baseline} .ui.repository.list .item .ui.header .metas span:not(:last-child){margin-right:5px} .ui.repository.list .item .time{font-size:12px;color:grey} .ui.repository.list .item .ui.tags{margin-bottom:1em} +.ui.repository.list .item .ui.avatar.image{width:24px;height:24px} .ui.repository.branches .time{font-size:12px;color:grey} .ui.user.list .item{padding-bottom:25px} .ui.user.list .item:not(:first-child){border-top:1px solid #eee;padding-top:25px} diff --git a/public/img/repo_default.png b/public/img/repo_default.png new file mode 100644 index 0000000000000..dbfa843723520 Binary files /dev/null and b/public/img/repo_default.png differ diff --git a/public/js/index.js b/public/js/index.js index 96d55eca877cd..96a56a42415c7 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -587,15 +587,14 @@ function initInstall() { var tidbDefault = 'data/gitea_tidb'; var dbType = $(this).val(); - if (dbType === "SQLite3" || dbType === "TiDB") { + if (dbType === "SQLite3") { $('#sql_settings').hide(); $('#pgsql_settings').hide(); + $('#mysql_settings').hide(); $('#sqlite_settings').show(); if (dbType === "SQLite3" && $('#db_path').val() == tidbDefault) { $('#db_path').val(sqliteDefault); - } else if (dbType === "TiDB" && $('#db_path').val() == sqliteDefault) { - $('#db_path').val(tidbDefault); } return; } @@ -610,6 +609,7 @@ function initInstall() { $('#sql_settings').show(); $('#pgsql_settings').toggle(dbType === "PostgreSQL"); + $('#mysql_settings').toggle(dbType === "MySQL"); $.each(dbDefaults, function(_type, defaultHost) { if ($('#db_host').val() == defaultHost) { $('#db_host').val(dbDefaults[dbType]); @@ -1048,6 +1048,10 @@ function initPullRequestReview() { $(this).closest('tr').removeClass('focus-lines-new focus-lines-old'); }); $('.add-code-comment').on('click', function(e) { + // https://github.com/go-gitea/gitea/issues/4745 + if ($(e.target).hasClass('btn-add-single')) { + return; + } e.preventDefault(); var isSplit = $(this).closest('.code-diff').hasClass('code-diff-split'); var side = $(this).data('side'); diff --git a/public/less/_explore.less b/public/less/_explore.less index 809a138a6ccc8..c5065a35bc240 100644 --- a/public/less/_explore.less +++ b/public/less/_explore.less @@ -53,6 +53,11 @@ .ui.tags { margin-bottom: 1em; } + + .ui.avatar.image { + width: 24px; + height: 24px; + } } } diff --git a/public/less/_repository.less b/public/less/_repository.less index 5970b366e2c85..9956bbce7492f 100644 --- a/public/less/_repository.less +++ b/public/less/_repository.less @@ -2219,6 +2219,15 @@ tbody.commit-list { white-space: pre-wrap; } +.git-notes { + &.top { + text-align: left; + } + .commit-body { + margin: 0; + } +} + @media only screen and (max-width: 767px) { .ui.stackable.menu { &.mobile--margin-between-items > .item { diff --git a/routers/admin/admin.go b/routers/admin/admin.go index 0e6fa2c2426e4..5107e18b7d98d 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -125,6 +125,7 @@ const ( reinitMissingRepository syncExternalUsers gitFsck + deleteGeneratedRepositoryAvatars ) // Dashboard show admin panel dashboard @@ -167,6 +168,9 @@ func Dashboard(ctx *context.Context) { case gitFsck: success = ctx.Tr("admin.dashboard.git_fsck_started") go models.GitFsck() + case deleteGeneratedRepositoryAvatars: + success = ctx.Tr("admin.dashboard.delete_generated_repository_avatars_success") + err = models.RemoveRandomAvatars() } if err != nil { diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index fba41a8cfece8..d740647cd4f20 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -45,6 +45,11 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) { return } + visibility := api.VisibleTypePublic + if form.Visibility != "" { + visibility = api.VisibilityModes[form.Visibility] + } + org := &models.User{ Name: form.UserName, FullName: form.FullName, @@ -53,7 +58,9 @@ func CreateOrg(ctx *context.APIContext, form api.CreateOrgOption) { Location: form.Location, IsActive: true, Type: models.UserTypeOrganization, + Visibility: visibility, } + if err := models.CreateOrganization(org, u); err != nil { if models.IsErrUserAlreadyExist(err) || models.IsErrNameReserved(err) || diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ae64e887caabd..c1561200cdbcc 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -608,7 +608,8 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/:username/:reponame", func() { m.Combo("").Get(reqAnyRepoReader(), repo.Get). - Delete(reqToken(), reqOwner(), repo.Delete) + Delete(reqToken(), reqOwner(), repo.Delete). + Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit) m.Group("/hooks", func() { m.Combo("").Get(repo.ListHooks). Post(bind(api.CreateHookOption{}), repo.CreateHook) diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go index 74fd9b3afd354..ba61c7e46c938 100644 --- a/routers/api/v1/convert/convert.go +++ b/routers/api/v1/convert/convert.go @@ -213,6 +213,7 @@ func ToOrganization(org *models.User) *api.Organization { Description: org.Description, Website: org.Website, Location: org.Location, + Visibility: org.Visibility.String(), } } diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index e1d0663f05aa4..2893887a4bf14 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -90,6 +90,11 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { return } + visibility := api.VisibleTypePublic + if form.Visibility != "" { + visibility = api.VisibilityModes[form.Visibility] + } + org := &models.User{ Name: form.UserName, FullName: form.FullName, @@ -98,6 +103,7 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { Location: form.Location, IsActive: true, Type: models.UserTypeOrganization, + Visibility: visibility, } if err := models.CreateOrganization(org, ctx.User); err != nil { if models.IsErrUserAlreadyExist(err) || @@ -153,6 +159,7 @@ func Edit(ctx *context.APIContext, form api.EditOrgOption) { // required: true // - name: body // in: body + // required: true // schema: // "$ref": "#/definitions/EditOrgOption" // responses: @@ -163,8 +170,11 @@ func Edit(ctx *context.APIContext, form api.EditOrgOption) { org.Description = form.Description org.Website = form.Website org.Location = form.Location - if err := models.UpdateUserCols(org, "full_name", "description", "website", "location"); err != nil { - ctx.Error(500, "UpdateUser", err) + if form.Visibility != "" { + org.Visibility = api.VisibilityModes[form.Visibility] + } + if err := models.UpdateUserCols(org, "full_name", "description", "website", "location", "visibility"); err != nil { + ctx.Error(500, "EditOrganization", err) return } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index db952263e2f23..20f80f37f4c67 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -181,7 +181,7 @@ func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) { // required: true // - name: body // in: body - // description: "'content' must be base64 encoded\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'sha' is the SHA for the file that already exists\n\n 'new_branch' (optional) will make a new branch from 'branch' before creating the file" + // required: true // schema: // "$ref": "#/definitions/CreateFileOptions" // responses: @@ -238,7 +238,7 @@ func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) { // required: true // - name: body // in: body - // description: "'content' must be base64 encoded\n\n 'sha' is the SHA for the file that already exists\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before updating the file" + // required: true // schema: // "$ref": "#/definitions/UpdateFileOptions" // responses: @@ -316,7 +316,7 @@ func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) { // required: true // - name: body // in: body - // description: "'sha' is the SHA for the file to be deleted\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before deleting the file" + // required: true // schema: // "$ref": "#/definitions/DeleteFileOptions" // responses: diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 62153893a6f38..f8df3e9fa12fe 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -240,6 +240,10 @@ func Create(ctx *context.APIContext, opt api.CreateRepoOption) { // responses: // "201": // "$ref": "#/responses/Repository" + // "409": + // description: The repository with the same name already exists. + // "422": + // "$ref": "#/responses/validationError" if ctx.User.IsOrganization() { // Shouldn't reach this condition, but just in case. ctx.Error(422, "", "not allowed creating repository for organization") @@ -500,6 +504,280 @@ func GetByID(ctx *context.APIContext) { ctx.JSON(200, repo.APIFormat(perm.AccessMode)) } +// Edit edit repository properties +func Edit(ctx *context.APIContext, opts api.EditRepoOption) { + // swagger:operation PATCH /repos/{owner}/{repo} repository repoEdit + // --- + // summary: Edit a repository's properties. Only fields that are set will be changed. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo to edit + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo to edit + // type: string + // required: true + // required: true + // - name: body + // in: body + // description: "Properties of a repo that you can edit" + // schema: + // "$ref": "#/definitions/EditRepoOption" + // responses: + // "200": + // "$ref": "#/responses/Repository" + // "403": + // "$ref": "#/responses/forbidden" + // "422": + // "$ref": "#/responses/validationError" + if err := updateBasicProperties(ctx, opts); err != nil { + return + } + + if err := updateRepoUnits(ctx, opts); err != nil { + return + } + + if opts.Archived != nil { + if err := updateRepoArchivedState(ctx, opts); err != nil { + return + } + } + + ctx.JSON(http.StatusOK, ctx.Repo.Repository.APIFormat(ctx.Repo.AccessMode)) +} + +// updateBasicProperties updates the basic properties of a repo: Name, Description, Website and Visibility +func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) error { + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + + oldRepoName := repo.Name + newRepoName := repo.Name + if opts.Name != nil { + newRepoName = *opts.Name + } + // Check if repository name has been changed and not just a case change + if repo.LowerName != strings.ToLower(newRepoName) { + if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil { + switch { + case models.IsErrRepoAlreadyExist(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is already taken [name: %s]", newRepoName), err) + case models.IsErrNameReserved(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name is reserved [name: %s]", newRepoName), err) + case models.IsErrNamePatternNotAllowed(err): + ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("repo name's pattern is not allowed [name: %s, pattern: %s]", newRepoName, err.(models.ErrNamePatternNotAllowed).Pattern), err) + default: + ctx.Error(http.StatusUnprocessableEntity, "ChangeRepositoryName", err) + } + return err + } + + err := models.NewRepoRedirect(ctx.Repo.Owner.ID, repo.ID, repo.Name, newRepoName) + if err != nil { + ctx.Error(http.StatusUnprocessableEntity, "NewRepoRedirect", err) + return err + } + + if err := models.RenameRepoAction(ctx.User, oldRepoName, repo); err != nil { + log.Error("RenameRepoAction: %v", err) + ctx.Error(http.StatusInternalServerError, "RenameRepoActions", err) + return err + } + + log.Trace("Repository name changed: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newRepoName) + } + // Update the name in the repo object for the response + repo.Name = newRepoName + repo.LowerName = strings.ToLower(newRepoName) + + if opts.Description != nil { + repo.Description = *opts.Description + } + + if opts.Website != nil { + repo.Website = *opts.Website + } + + visibilityChanged := false + if opts.Private != nil { + // Visibility of forked repository is forced sync with base repository. + if repo.IsFork { + *opts.Private = repo.BaseRepo.IsPrivate + } + + visibilityChanged = repo.IsPrivate != *opts.Private + // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public + if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.User.IsAdmin { + err := fmt.Errorf("cannot change private repository to public") + ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err) + return err + } + + repo.IsPrivate = *opts.Private + } + + if err := models.UpdateRepository(repo, visibilityChanged); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) + return err + } + + log.Trace("Repository basic settings updated: %s/%s", owner.Name, repo.Name) + return nil +} + +func unitTypeInTypes(unitType models.UnitType, unitTypes []models.UnitType) bool { + for _, tp := range unitTypes { + if unitType == tp { + return true + } + } + return false +} + +// updateRepoUnits updates repo units: Issue settings, Wiki settings, PR settings +func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error { + owner := ctx.Repo.Owner + repo := ctx.Repo.Repository + + var units []models.RepoUnit + + for _, tp := range models.MustRepoUnits { + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: tp, + Config: new(models.UnitConfig), + }) + } + + if opts.HasIssues != nil { + if *opts.HasIssues { + // We don't currently allow setting individual issue settings through the API, + // only can enable/disable issues, so when enabling issues, + // we either get the existing config which means it was already enabled, + // or create a new config since it doesn't exist. + unit, err := repo.GetUnit(models.UnitTypeIssues) + var config *models.IssuesConfig + if err != nil { + // Unit type doesn't exist so we make a new config file with default values + config = &models.IssuesConfig{ + EnableTimetracker: true, + AllowOnlyContributorsToTrackTime: true, + EnableDependencies: true, + } + } else { + config = unit.IssuesConfig() + } + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypeIssues, + Config: config, + }) + } + } + + if opts.HasWiki != nil { + if *opts.HasWiki { + // We don't currently allow setting individual wiki settings through the API, + // only can enable/disable the wiki, so when enabling the wiki, + // we either get the existing config which means it was already enabled, + // or create a new config since it doesn't exist. + config := &models.UnitConfig{} + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypeWiki, + Config: config, + }) + } + } + + if opts.HasPullRequests != nil { + if *opts.HasPullRequests { + // We do allow setting individual PR settings through the API, so + // we get the config settings and then set them + // if those settings were provided in the opts. + unit, err := repo.GetUnit(models.UnitTypePullRequests) + var config *models.PullRequestsConfig + if err != nil { + // Unit type doesn't exist so we make a new config file with default values + config = &models.PullRequestsConfig{ + IgnoreWhitespaceConflicts: false, + AllowMerge: true, + AllowRebase: true, + AllowRebaseMerge: true, + AllowSquash: true, + } + } else { + config = unit.PullRequestsConfig() + } + + if opts.IgnoreWhitespaceConflicts != nil { + config.IgnoreWhitespaceConflicts = *opts.IgnoreWhitespaceConflicts + } + if opts.AllowMerge != nil { + config.AllowMerge = *opts.AllowMerge + } + if opts.AllowRebase != nil { + config.AllowRebase = *opts.AllowRebase + } + if opts.AllowRebaseMerge != nil { + config.AllowRebaseMerge = *opts.AllowRebaseMerge + } + if opts.AllowSquash != nil { + config.AllowSquash = *opts.AllowSquash + } + + units = append(units, models.RepoUnit{ + RepoID: repo.ID, + Type: models.UnitTypePullRequests, + Config: config, + }) + } + } + + if err := models.UpdateRepositoryUnits(repo, units); err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateRepositoryUnits", err) + return err + } + + log.Trace("Repository advanced settings updated: %s/%s", owner.Name, repo.Name) + return nil +} + +// updateRepoArchivedState updates repo's archive state +func updateRepoArchivedState(ctx *context.APIContext, opts api.EditRepoOption) error { + repo := ctx.Repo.Repository + // archive / un-archive + if opts.Archived != nil { + if repo.IsMirror { + err := fmt.Errorf("repo is a mirror, cannot archive/un-archive") + ctx.Error(http.StatusUnprocessableEntity, err.Error(), err) + return err + } + if *opts.Archived { + if err := repo.SetArchiveRepoState(*opts.Archived); err != nil { + log.Error("Tried to archive a repo: %s", err) + ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + return err + } + log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + } else { + if err := repo.SetArchiveRepoState(*opts.Archived); err != nil { + log.Error("Tried to un-archive a repo: %s", err) + ctx.Error(http.StatusInternalServerError, "ArchiveRepoState", err) + return err + } + log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) + } + } + return nil +} + // Delete one repository func Delete(ctx *context.APIContext) { // swagger:operation DELETE /repos/{owner}/{repo} repository repoDelete diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go new file mode 100644 index 0000000000000..053134ec6199b --- /dev/null +++ b/routers/api/v1/repo/repo_test.go @@ -0,0 +1,82 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package repo + +import ( + "net/http" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRepoEdit(t *testing.T) { + models.PrepareTestEnv(t) + + ctx := test.MockContext(t, "user2/repo1") + test.LoadRepo(t, ctx, 1) + test.LoadUser(t, ctx, 2) + ctx.Repo.Owner = ctx.User + description := "new description" + website := "http://wwww.newwebsite.com" + private := true + hasIssues := false + hasWiki := false + defaultBranch := "master" + hasPullRequests := true + ignoreWhitespaceConflicts := true + allowMerge := false + allowRebase := false + allowRebaseMerge := false + allowSquashMerge := false + archived := true + opts := api.EditRepoOption{ + Name: &ctx.Repo.Repository.Name, + Description: &description, + Website: &website, + Private: &private, + HasIssues: &hasIssues, + HasWiki: &hasWiki, + DefaultBranch: &defaultBranch, + HasPullRequests: &hasPullRequests, + IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts, + AllowMerge: &allowMerge, + AllowRebase: &allowRebase, + AllowRebaseMerge: &allowRebaseMerge, + AllowSquash: &allowSquashMerge, + Archived: &archived, + } + + Edit(&context.APIContext{Context: ctx, Org: nil}, opts) + + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + models.AssertExistsAndLoadBean(t, &models.Repository{ + ID: 1, + }, models.Cond("name = ? AND is_archived = 1", *opts.Name)) +} + +func TestRepoEditNameChange(t *testing.T) { + models.PrepareTestEnv(t) + + ctx := test.MockContext(t, "user2/repo1") + test.LoadRepo(t, ctx, 1) + test.LoadUser(t, ctx, 2) + ctx.Repo.Owner = ctx.User + name := "newname" + opts := api.EditRepoOption{ + Name: &name, + } + + Edit(&context.APIContext{Context: ctx, Org: nil}, opts) + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + + models.AssertExistsAndLoadBean(t, &models.Repository{ + ID: 1, + }, models.Cond("name = ?", opts.Name)) +} diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 2df97304aa060..c1196eeb71581 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -82,6 +82,8 @@ type swaggerParameterBodies struct { // in:body CreateRepoOption api.CreateRepoOption // in:body + EditRepoOption api.EditRepoOption + // in:body CreateForkOption api.CreateForkOption // in:body diff --git a/routers/init.go b/routers/init.go index 88422cc6ede03..b3078b478aff0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -5,7 +5,6 @@ package routers import ( - "path" "strings" "time" @@ -19,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/mailer" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/external" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/ssh" @@ -75,6 +75,7 @@ func GlobalInit() { if setting.InstallLock { highlight.NewContext() + external.RegisterParsers() markup.Init() if err := initDBEngine(); err == nil { log.Info("ORM engine initialization successful!") @@ -97,7 +98,6 @@ func GlobalInit() { models.InitSyncMirrors() models.InitDeliverHooks() models.InitTestPullRequests() - log.NewGitLogger(path.Join(setting.LogRootPath, "http.log")) } if models.EnableSQLite3 { log.Info("SQLite3 Supported") diff --git a/routers/install.go b/routers/install.go index 28bca2b4f7243..c95abebea76c2 100644 --- a/routers/install.go +++ b/routers/install.go @@ -57,6 +57,7 @@ func Install(ctx *context.Context) { form.DbPasswd = models.DbCfg.Passwd form.DbName = models.DbCfg.Name form.DbPath = models.DbCfg.Path + form.Charset = models.DbCfg.Charset ctx.Data["CurDbOption"] = "MySQL" switch models.DbCfg.Type { @@ -150,6 +151,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { models.DbCfg.Passwd = form.DbPasswd models.DbCfg.Name = form.DbName models.DbCfg.SSLMode = form.SSLMode + models.DbCfg.Charset = form.Charset models.DbCfg.Path = form.DbPath if (models.DbCfg.Type == "sqlite3") && @@ -213,18 +215,42 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { return } - // Check admin password. - if len(form.AdminName) > 0 && len(form.AdminPasswd) == 0 { - ctx.Data["Err_Admin"] = true - ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) - return - } - if form.AdminPasswd != form.AdminConfirmPasswd { - ctx.Data["Err_Admin"] = true - ctx.Data["Err_AdminPasswd"] = true - ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) - return + // Check admin user creation + if len(form.AdminName) > 0 { + // Ensure AdminName is valid + if err := models.IsUsableUsername(form.AdminName); err != nil { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminName"] = true + if models.IsErrNameReserved(err) { + ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form) + return + } else if models.IsErrNamePatternNotAllowed(err) { + ctx.RenderWithErr(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form) + return + } + ctx.RenderWithErr(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form) + return + } + // Check Admin email + if len(form.AdminEmail) == 0 { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminEmail"] = true + ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_email"), tplInstall, form) + return + } + // Check admin password. + if len(form.AdminPasswd) == 0 { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminPasswd"] = true + ctx.RenderWithErr(ctx.Tr("install.err_empty_admin_password"), tplInstall, form) + return + } + if form.AdminPasswd != form.AdminConfirmPasswd { + ctx.Data["Err_Admin"] = true + ctx.Data["Err_AdminPasswd"] = true + ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) + return + } } if form.AppURL[len(form.AppURL)-1] != '/' { @@ -245,6 +271,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { cfg.Section("database").Key("USER").SetValue(models.DbCfg.User) cfg.Section("database").Key("PASSWD").SetValue(models.DbCfg.Passwd) cfg.Section("database").Key("SSL_MODE").SetValue(models.DbCfg.SSLMode) + cfg.Section("database").Key("CHARSET").SetValue(models.DbCfg.Charset) cfg.Section("database").Key("PATH").SetValue(models.DbCfg.Path) cfg.Section("").Key("APP_NAME").SetValue(form.AppName) diff --git a/routers/private/branch.go b/routers/private/branch.go deleted file mode 100644 index e883607b898e6..0000000000000 --- a/routers/private/branch.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "code.gitea.io/gitea/models" - - macaron "gopkg.in/macaron.v1" -) - -// GetProtectedBranchBy get protected branch information -func GetProtectedBranchBy(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":id") - branchName := ctx.Params("*") - protectBranch, err := models.GetProtectedBranchBy(repoID, branchName) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } else if protectBranch != nil { - ctx.JSON(200, protectBranch) - } else { - ctx.JSON(200, &models.ProtectedBranch{ - ID: 0, - }) - } -} - -// CanUserPush returns if user push -func CanUserPush(ctx *macaron.Context) { - pbID := ctx.ParamsInt64(":pbid") - userID := ctx.ParamsInt64(":userid") - - protectBranch, err := models.GetProtectedBranchByID(pbID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } else if protectBranch != nil { - ctx.JSON(200, map[string]interface{}{ - "can_push": protectBranch.CanUserPush(userID), - }) - } else { - ctx.JSON(200, map[string]interface{}{ - "can_push": false, - }) - } -} - -// HasEnoughApprovals return if PR has enough approvals -func HasEnoughApprovals(ctx *macaron.Context) { - pbID := ctx.ParamsInt64(":pbid") - prID := ctx.ParamsInt64(":prid") - - protectBranch, err := models.GetProtectedBranchByID(pbID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } else if prID > 0 && protectBranch != nil { - pr, err := models.GetPullRequestByID(prID) - if err == nil { - err = pr.LoadAttributes() - } - if err == nil { - err = pr.LoadIssue() - } - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - ctx.JSON(200, map[string]interface{}{ - "can_push": protectBranch.HasEnoughApprovals(pr), - }) - } else { - ctx.JSON(200, map[string]interface{}{ - "can_push": false, - }) - } -} diff --git a/routers/private/hook.go b/routers/private/hook.go new file mode 100644 index 0000000000000..25f5720776012 --- /dev/null +++ b/routers/private/hook.go @@ -0,0 +1,227 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. +package private + +import ( + "fmt" + "net/http" + "os" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/util" + + macaron "gopkg.in/macaron.v1" +) + +// HookPreReceive checks whether a individual commit is acceptable +func HookPreReceive(ctx *macaron.Context) { + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + oldCommitID := ctx.QueryTrim("old") + newCommitID := ctx.QueryTrim("new") + refFullName := ctx.QueryTrim("ref") + userID := ctx.QueryInt64("userID") + gitObjectDirectory := ctx.QueryTrim("gitObjectDirectory") + gitAlternativeObjectDirectories := ctx.QueryTrim("gitAlternativeObjectDirectories") + prID := ctx.QueryInt64("prID") + + branchName := strings.TrimPrefix(refFullName, git.BranchPrefix) + repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) + if err != nil { + log.Error("Unable to get repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err.Error(), + }) + return + } + repo.OwnerName = ownerName + protectBranch, err := models.GetProtectedBranchBy(repo.ID, branchName) + if err != nil { + log.Error("Unable to get protected branch: %s in %-v Error: %v", branchName, repo, err) + ctx.JSON(500, map[string]interface{}{ + "err": err.Error(), + }) + return + } + if protectBranch != nil && protectBranch.IsProtected() { + // check and deletion + if newCommitID == git.EmptySHA { + log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from deletion", branchName), + }) + return + } + + // detect force push + if git.EmptySHA != oldCommitID { + env := append(os.Environ(), + private.GitAlternativeObjectDirectories+"="+gitAlternativeObjectDirectories, + private.GitObjectDirectory+"="+gitObjectDirectory, + private.GitQuarantinePath+"="+gitObjectDirectory, + ) + + output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDirWithEnv(repo.RepoPath(), env) + if err != nil { + log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, newCommitID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Fail to detect force push: %v", err), + }) + return + } else if len(output) > 0 { + log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("branch %s is protected from force push", branchName), + }) + return + + } + } + + canPush := protectBranch.CanUserPush(userID) + if !canPush && prID > 0 { + pr, err := models.GetPullRequestByID(prID) + if err != nil { + log.Error("Unable to get PullRequest %d Error: %v", prID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Unable to get PullRequest %d Error: %v", prID, err) + }) + return + } + if !protectBranch.HasEnoughApprovals(pr) { + log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v and pr #%d does not have enough approvals", userID, branchName, repo, pr.Index) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("protected branch %s can not be pushed to and pr #%d does not have enough approvals", branchName, prID), + }) + return + } + } else if !canPush { + log.Warn("Forbidden: User %d cannot push to protected branch: %s in %-v", userID, branchName, repo) + ctx.JSON(http.StatusForbidden, map[string]interface{}{ + "err": fmt.Sprintf("protected branch %s can not be pushed to", branchName), + }) + return + } + } + ctx.PlainText(http.StatusOK, []byte("ok")) +} + +// HookPostReceive updates services and users +func HookPostReceive(ctx *macaron.Context) { + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + oldCommitID := ctx.Query("old") + newCommitID := ctx.Query("new") + refFullName := ctx.Query("ref") + userID := ctx.QueryInt64("userID") + userName := ctx.Query("username") + + branch := refFullName + if strings.HasPrefix(refFullName, git.BranchPrefix) { + branch = strings.TrimPrefix(refFullName, git.BranchPrefix) + } else if strings.HasPrefix(refFullName, git.TagPrefix) { + branch = strings.TrimPrefix(refFullName, git.TagPrefix) + } + + // Only trigger activity updates for changes to branches or + // tags. Updates to other refs (eg, refs/notes, refs/changes, + // or other less-standard refs spaces are ignored since there + // may be a very large number of them). + if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { + if err := models.PushUpdate(branch, models.PushUpdateOptions{ + RefFullName: refFullName, + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + PusherID: userID, + PusherName: userName, + RepoUserName: ownerName, + RepoName: repoName, + }); err != nil { + log.Error("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Failed to Update: %s/%s Branch: %s Error: %v", ownerName, repoName, branch, err), + }) + return + } + } + + if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) { + repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) + if err != nil { + log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + repo.OwnerName = ownerName + + pullRequestAllowed := repo.AllowsPulls() + if !pullRequestAllowed { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": false, + }) + return + } + + baseRepo := repo + if repo.IsFork { + if err := repo.GetBaseRepo(); err != nil { + log.Error("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf("Failed to get Base Repository of Forked repository: %-v Error: %v", repo, err), + }) + return + } + baseRepo = repo.BaseRepo + } + + if !repo.IsFork && branch == baseRepo.DefaultBranch { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": false, + }) + return + } + + pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch) + if err != nil && !models.IsErrPullRequestNotExist(err) { + log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": fmt.Sprintf( + "Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err), + }) + return + } + + if pr == nil { + if repo.IsFork { + branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch) + } + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": true, + "create": true, + "branch": branch, + "url": fmt.Sprintf("%s/compare/%s...%s", baseRepo.HTMLURL(), util.PathEscapeSegments(baseRepo.DefaultBranch), util.PathEscapeSegments(branch)), + }) + } else { + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": true, + "create": false, + "branch": branch, + "url": fmt.Sprintf("%s/pulls/%d", baseRepo.HTMLURL(), pr.Index), + }) + } + return + } + ctx.JSON(http.StatusOK, map[string]interface{}{ + "message": false, + }) + return +} diff --git a/routers/private/internal.go b/routers/private/internal.go index 0cc088fe6b77a..11cea8b4b9f3b 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -76,20 +76,10 @@ func CheckUnitUser(ctx *macaron.Context) { // These APIs will be invoked by internal commands for example `gitea serv` and etc. func RegisterRoutes(m *macaron.Macaron) { m.Group("/", func() { - m.Get("/ssh/:id", GetPublicKeyByID) - m.Get("/ssh/:id/user", GetUserByKeyID) - m.Post("/ssh/:id/update", UpdatePublicKey) - m.Post("/repositories/:repoid/keys/:keyid/update", UpdateDeployKey) - m.Get("/repositories/:repoid/user/:userid/checkunituser", CheckUnitUser) - m.Get("/repositories/:repoid/has-keys/:keyid", HasDeployKey) - m.Get("/repositories/:repoid/keys/:keyid", GetDeployKey) - m.Get("/repositories/:repoid/wiki/init", InitWiki) - m.Post("/push/update", PushUpdate) - m.Get("/protectedbranch/:pbid/:userid", CanUserPush) - m.Get("/protectedbranchpr/:pbid/:prid", HasEnoughApprovals) - m.Get("/repo/:owner/:repo", GetRepositoryByOwnerAndName) - m.Get("/branch/:id/*", GetProtectedBranchBy) - m.Get("/repository/:rid", GetRepository) - m.Get("/active-pull-request", GetActivePullRequest) + m.Post("/ssh/:id/update/:repoid", UpdatePublicKeyInRepo) + m.Get("/hook/pre-receive/:owner/:repo", HookPreReceive) + m.Get("/hook/post-receive/:owner/:repo", HookPostReceive) + m.Get("/serv/none/:keyid", ServNoCommand) + m.Get("/serv/command/:keyid/:owner/:repo", ServCommand) }, CheckInternalToken) } diff --git a/routers/private/key.go b/routers/private/key.go index ee22f6ac4881c..f7212ec8929f1 100644 --- a/routers/private/key.go +++ b/routers/private/key.go @@ -12,30 +12,10 @@ import ( macaron "gopkg.in/macaron.v1" ) -// UpdateDeployKey update deploy key updates -func UpdateDeployKey(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":repoid") - keyID := ctx.ParamsInt64(":keyid") - deployKey, err := models.GetDeployKeyByRepo(keyID, repoID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - deployKey.UpdatedUnix = util.TimeStampNow() - if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - ctx.PlainText(200, []byte("success")) -} - -// UpdatePublicKey update publick key updates -func UpdatePublicKey(ctx *macaron.Context) { +// UpdatePublicKeyInRepo update public key and deploy key updates +func UpdatePublicKeyInRepo(ctx *macaron.Context) { keyID := ctx.ParamsInt64(":id") + repoID := ctx.ParamsInt64(":repoid") if err := models.UpdatePublicKeyUpdated(keyID); err != nil { ctx.JSON(500, map[string]interface{}{ "err": err.Error(), @@ -43,60 +23,24 @@ func UpdatePublicKey(ctx *macaron.Context) { return } - ctx.PlainText(200, []byte("success")) -} - -//GetPublicKeyByID chainload to models.GetPublicKeyByID -func GetPublicKeyByID(ctx *macaron.Context) { - keyID := ctx.ParamsInt64(":id") - key, err := models.GetPublicKeyByID(keyID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - ctx.JSON(200, key) -} - -//GetUserByKeyID chainload to models.GetUserByKeyID -func GetUserByKeyID(ctx *macaron.Context) { - keyID := ctx.ParamsInt64(":id") - user, err := models.GetUserByKeyID(keyID) + deployKey, err := models.GetDeployKeyByRepo(keyID, repoID) if err != nil { + if models.IsErrDeployKeyNotExist(err) { + ctx.PlainText(200, []byte("success")) + return + } ctx.JSON(500, map[string]interface{}{ "err": err.Error(), }) return } - ctx.JSON(200, user) -} - -//GetDeployKey chainload to models.GetDeployKey -func GetDeployKey(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":repoid") - keyID := ctx.ParamsInt64(":keyid") - dKey, err := models.GetDeployKeyByRepo(keyID, repoID) - if err != nil { - if models.IsErrDeployKeyNotExist(err) { - ctx.JSON(404, []byte("not found")) - return - } + deployKey.UpdatedUnix = util.TimeStampNow() + if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil { ctx.JSON(500, map[string]interface{}{ "err": err.Error(), }) return } - ctx.JSON(200, dKey) -} -//HasDeployKey chainload to models.HasDeployKey -func HasDeployKey(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":repoid") - keyID := ctx.ParamsInt64(":keyid") - if models.HasDeployKey(keyID, repoID) { - ctx.PlainText(200, []byte("success")) - return - } - ctx.PlainText(404, []byte("not found")) + ctx.PlainText(200, []byte("success")) } diff --git a/routers/private/push_update.go b/routers/private/push_update.go deleted file mode 100644 index 5c42f066ee7d5..0000000000000 --- a/routers/private/push_update.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "encoding/json" - "strings" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" - - macaron "gopkg.in/macaron.v1" -) - -// PushUpdate update public key updates -func PushUpdate(ctx *macaron.Context) { - var opt models.PushUpdateOptions - if err := json.NewDecoder(ctx.Req.Request.Body).Decode(&opt); err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - branch := strings.TrimPrefix(opt.RefFullName, git.BranchPrefix) - if len(branch) == 0 || opt.PusherID <= 0 { - ctx.Error(404) - log.Trace("PushUpdate: branch or secret is empty, or pusher ID is not valid") - return - } - - err := models.PushUpdate(branch, opt) - if err != nil { - if models.IsErrUserNotExist(err) { - ctx.Error(404) - } else { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - } - return - } - ctx.Status(202) -} diff --git a/routers/private/repository.go b/routers/private/repository.go deleted file mode 100644 index 9f451bcf1dbb0..0000000000000 --- a/routers/private/repository.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "net/http" - - "code.gitea.io/gitea/models" - - macaron "gopkg.in/macaron.v1" -) - -// GetRepository return the default branch of a repository -func GetRepository(ctx *macaron.Context) { - repoID := ctx.ParamsInt64(":rid") - repository, err := models.GetRepositoryByID(repoID) - repository.MustOwnerName() - allowPulls := repository.AllowsPulls() - // put it back to nil because json unmarshal can't unmarshal it - repository.Units = nil - - if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - if repository.IsFork { - repository.GetBaseRepo() - if err != nil { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - repository.BaseRepo.MustOwnerName() - allowPulls = repository.BaseRepo.AllowsPulls() - // put it back to nil because json unmarshal can't unmarshal it - repository.BaseRepo.Units = nil - } - - ctx.JSON(http.StatusOK, struct { - Repository *models.Repository - AllowPullRequest bool - }{ - Repository: repository, - AllowPullRequest: allowPulls, - }) -} - -// GetActivePullRequest return an active pull request when it exists or an empty object -func GetActivePullRequest(ctx *macaron.Context) { - baseRepoID := ctx.QueryInt64("baseRepoID") - headRepoID := ctx.QueryInt64("headRepoID") - baseBranch := ctx.QueryTrim("baseBranch") - if len(baseBranch) == 0 { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": "QueryTrim failed", - }) - return - } - - headBranch := ctx.QueryTrim("headBranch") - if len(headBranch) == 0 { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": "QueryTrim failed", - }) - return - } - - pr, err := models.GetUnmergedPullRequest(headRepoID, baseRepoID, headBranch, baseBranch) - if err != nil && !models.IsErrPullRequestNotExist(err) { - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - ctx.JSON(http.StatusOK, pr) -} diff --git a/routers/private/serv.go b/routers/private/serv.go new file mode 100644 index 0000000000000..68e4361e56d84 --- /dev/null +++ b/routers/private/serv.go @@ -0,0 +1,286 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +// Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. +package private + +import ( + "fmt" + "net/http" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/modules/setting" + + macaron "gopkg.in/macaron.v1" +) + +// ServNoCommand returns information about the provided keyid +func ServNoCommand(ctx *macaron.Context) { + keyID := ctx.ParamsInt64(":keyid") + if keyID <= 0 { + ctx.JSON(http.StatusBadRequest, map[string]interface{}{ + "err": fmt.Sprintf("Bad key id: %d", keyID), + }) + } + results := private.KeyAndOwner{} + + key, err := models.GetPublicKeyByID(keyID) + if err != nil { + if models.IsErrKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "err": fmt.Sprintf("Cannot find key: %d", keyID), + }) + return + } + log.Error("Unable to get public key: %d Error: %v", keyID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err.Error(), + }) + return + } + results.Key = key + + if key.Type == models.KeyTypeUser { + user, err := models.GetUserByID(key.OwnerID) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "err": fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), + }) + return + } + log.Error("Unable to get owner with id: %d for public key: %d Error: %v", key.OwnerID, keyID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err.Error(), + }) + return + } + results.Owner = user + } + ctx.JSON(http.StatusOK, &results) + return +} + +// ServCommand returns information about the provided keyid +func ServCommand(ctx *macaron.Context) { + // Although we provide the verbs we don't need them at present they're just for logging purposes + keyID := ctx.ParamsInt64(":keyid") + ownerName := ctx.Params(":owner") + repoName := ctx.Params(":repo") + mode := models.AccessMode(ctx.QueryInt("mode")) + + // Set the basic parts of the results to return + results := private.ServCommandResults{ + RepoName: repoName, + OwnerName: ownerName, + KeyID: keyID, + } + + // Now because we're not translating things properly let's just default some Engish strings here + modeString := "read" + if mode > models.AccessModeRead { + modeString = "write to" + } + + // The default unit we're trying to look at is code + unitType := models.UnitTypeCode + + // Unless we're a wiki... + if strings.HasSuffix(repoName, ".wiki") { + // in which case we need to look at the wiki + unitType = models.UnitTypeWiki + // And we'd better munge the reponame and tell downstream we're looking at a wiki + results.IsWiki = true + results.RepoName = repoName[:len(repoName)-5] + } + + // Now get the Repository and set the results section + repo, err := models.GetRepositoryByOwnerAndName(results.OwnerName, results.RepoName) + if err != nil { + if models.IsErrRepoNotExist(err) { + ctx.JSON(http.StatusNotFound, map[string]interface{}{ + "results": results, + "type": "ErrRepoNotExist", + "err": fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), + }) + return + } + log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), + }) + return + } + repo.OwnerName = ownerName + results.RepoID = repo.ID + + // We can shortcut at this point if the repo is a mirror + if mode > models.AccessModeRead && repo.IsMirror { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrMirrorReadOnly", + "err": fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), + }) + return + } + + // Get the Public Key represented by the keyID + key, err := models.GetPublicKeyByID(keyID) + if err != nil { + if models.IsErrKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrKeyNotExist", + "err": fmt.Sprintf("Cannot find key: %d", keyID), + }) + return + } + log.Error("Unable to get public key: %d Error: %v", keyID, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), + }) + return + } + results.KeyName = key.Name + results.KeyID = key.ID + results.UserID = key.OwnerID + + // Deploy Keys have ownerID set to 0 therefore we can't use the owner + // So now we need to check if the key is a deploy key + // We'll keep hold of the deploy key here for permissions checking + var deployKey *models.DeployKey + var user *models.User + if key.Type == models.KeyTypeDeploy { + results.IsDeployKey = true + + var err error + deployKey, err = models.GetDeployKeyByRepo(key.ID, repo.ID) + if err != nil { + if models.IsErrDeployKeyNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrDeployKeyNotExist", + "err": fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + }) + return + } + log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), + }) + return + } + results.KeyName = deployKey.Name + + // FIXME: Deploy keys aren't really the owner of the repo pushing changes + // however we don't have good way of representing deploy keys in hook.go + // so for now use the owner of the repository + results.UserName = results.OwnerName + results.UserID = repo.OwnerID + } else { + // Get the user represented by the Key + var err error + user, err = models.GetUserByID(key.OwnerID) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrUserNotExist", + "err": fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), + }) + return + } + log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), + }) + return + } + results.UserName = user.Name + } + + // Don't allow pushing if the repo is archived + if mode > models.AccessModeRead && repo.IsArchived { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrRepoIsArchived", + "err": fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), + }) + return + } + + // Permissions checking: + if mode > models.AccessModeRead || repo.IsPrivate || setting.Service.RequireSignInView { + if key.Type == models.KeyTypeDeploy { + if deployKey.Mode < mode { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrUnauthorized", + "err": fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), + }) + return + } + } else { + perm, err := models.GetUserRepoPermission(repo, user) + if err != nil { + log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), + }) + return + } + + userMode := perm.UnitAccessMode(unitType) + + if userMode < mode { + ctx.JSON(http.StatusUnauthorized, map[string]interface{}{ + "results": results, + "type": "ErrUnauthorized", + "err": fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), + }) + return + } + } + } + + // Finally if we're trying to touch the wiki we should init it + if results.IsWiki { + if err = repo.InitWiki(); err != nil { + log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "results": results, + "type": "InternalServerError", + "err": fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), + }) + return + } + } + log.Debug("Serv Results:\nIsWiki: %t\nIsDeployKey: %t\nKeyID: %d\tKeyName: %s\nUserName: %s\nUserID: %d\nOwnerName: %s\nRepoName: %s\nRepoID: %d", + results.IsWiki, + results.IsDeployKey, + results.KeyID, + results.KeyName, + results.UserName, + results.UserID, + results.OwnerName, + results.RepoName, + results.RepoID) + + ctx.JSON(http.StatusOK, results) + // We will update the keys in a different call. + return +} diff --git a/routers/private/wiki.go b/routers/private/wiki.go deleted file mode 100644 index 33bcbaf17ea6e..0000000000000 --- a/routers/private/wiki.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package private - -import ( - "code.gitea.io/gitea/models" - - macaron "gopkg.in/macaron.v1" -) - -// InitWiki initilizes wiki via repo id -func InitWiki(ctx *macaron.Context) { - repoID := ctx.ParamsInt64("repoid") - - repo, err := models.GetRepositoryByID(repoID) - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - err = repo.InitWiki() - if err != nil { - ctx.JSON(500, map[string]interface{}{ - "err": err.Error(), - }) - return - } - - ctx.Status(202) -} diff --git a/routers/repo/commit.go b/routers/repo/commit.go index 2978eda6c0105..870ff568f3516 100644 --- a/routers/repo/commit.go +++ b/routers/repo/commit.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" ) const ( @@ -246,6 +247,15 @@ func Diff(ctx *context.Context) { ctx.Data["Parents"] = parents ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0 ctx.Data["SourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", commitID) + + note := &git.Note{} + err = git.GetNote(ctx.Repo.GitRepo, commitID, note) + if err == nil { + ctx.Data["Note"] = string(templates.ToUTF8WithFallback(note.Message)) + ctx.Data["NoteCommit"] = note.Commit + ctx.Data["NoteAuthor"] = models.ValidateCommitWithEmail(note.Commit) + } + if commit.ParentCount() > 0 { ctx.Data["BeforeSourcePath"] = setting.AppSubURL + "/" + path.Join(userName, repoName, "src", "commit", parents[0]) } diff --git a/routers/repo/http.go b/routers/repo/http.go index fccecfb71d2b2..214e2f3411330 100644 --- a/routers/repo/http.go +++ b/routers/repo/http.go @@ -351,7 +351,7 @@ func gitCommand(dir string, args ...string) []byte { cmd.Dir = dir out, err := cmd.Output() if err != nil { - log.GitLogger.Error(fmt.Sprintf("%v - %s", err, out)) + log.Error("%v - %s", err, out) } return out } @@ -409,7 +409,7 @@ func serviceRPC(h serviceHandler, service string) { if h.r.Header.Get("Content-Encoding") == "gzip" { reqBody, err = gzip.NewReader(reqBody) if err != nil { - log.GitLogger.Error("Fail to create gzip reader: %v", err) + log.Error("Fail to create gzip reader: %v", err) h.w.WriteHeader(http.StatusInternalServerError) return } @@ -428,7 +428,7 @@ func serviceRPC(h serviceHandler, service string) { cmd.Stdin = reqBody cmd.Stderr = &stderr if err := cmd.Run(); err != nil { - log.GitLogger.Error("Fail to serve RPC(%s): %v - %v", service, err, stderr) + log.Error("Fail to serve RPC(%s): %v - %v", service, err, stderr) return } } @@ -541,7 +541,7 @@ func HTTPBackend(ctx *context.Context, cfg *serviceConfig) http.HandlerFunc { file := strings.Replace(r.URL.Path, m[1]+"/", "", 1) dir, err := getGitRepoPath(m[1]) if err != nil { - log.GitLogger.Error(err.Error()) + log.Error(err.Error()) ctx.NotFound("HTTPBackend", err) return } diff --git a/routers/repo/setting.go b/routers/repo/setting.go index f58601633a222..767cdacde0195 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -7,11 +7,14 @@ package repo import ( "errors" + "fmt" + "io/ioutil" "net/url" "regexp" "strings" "time" + "github.com/Unknwon/com" "mvdan.cc/xurls/v2" "code.gitea.io/gitea/models" @@ -246,7 +249,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.Redirect(repo.Link() + "/settings") return } - if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalURL(form.TrackerURLFormat) { + if len(form.TrackerURLFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(form.TrackerURLFormat) { ctx.Flash.Error(ctx.Tr("repo.settings.tracker_url_format_error")) ctx.Redirect(repo.Link() + "/settings") return @@ -727,3 +730,59 @@ func init() { panic(err) } } + +// UpdateAvatarSetting update repo's avatar +func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm) error { + ctxRepo := ctx.Repo.Repository + + if form.Avatar == nil { + // No avatar is uploaded and we not removing it here. + // No random avatar generated here. + // Just exit, no action. + if !com.IsFile(ctxRepo.CustomAvatarPath()) { + log.Trace("No avatar was uploaded for repo: %d. Default icon will appear instead.", ctxRepo.ID) + } + return nil + } + + r, err := form.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer r.Close() + + if form.Avatar.Size > setting.AvatarMaxFileSize { + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + } + + data, err := ioutil.ReadAll(r) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !base.IsImageFile(data) { + return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxRepo.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + return nil +} + +// SettingsAvatar save new POSTed repository avatar +func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) { + form.Source = auth.AvatarLocal + if err := UpdateAvatarSetting(ctx, form); err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("repo.settings.update_avatar_success")) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} + +// SettingsDeleteAvatar delete repository avatar +func SettingsDeleteAvatar(ctx *context.Context) { + if err := ctx.Repo.Repository.DeleteAvatar(); err != nil { + ctx.Flash.Error(fmt.Sprintf("DeleteAvatar: %v", err)) + } + ctx.Redirect(ctx.Repo.RepoLink + "/settings") +} diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 5a5fc518b9277..eb5f73768e38e 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -159,6 +159,14 @@ func NewMacaron() *macaron.Macaron { ExpiresAfter: time.Hour * 6, }, )) + m.Use(public.StaticHandler( + setting.RepositoryAvatarUploadPath, + &public.Options{ + Prefix: "repo-avatars", + SkipLogging: setting.DisableRouterLog, + ExpiresAfter: time.Hour * 6, + }, + )) m.Use(templates.HTMLRenderer()) models.InitMailRender(templates.Mailer()) @@ -613,6 +621,9 @@ func RegisterRoutes(m *macaron.Macaron) { m.Group("/settings", func() { m.Combo("").Get(repo.Settings). Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost) + m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), repo.SettingsAvatar) + m.Post("/avatar/delete", repo.SettingsDeleteAvatar) + m.Group("/collaboration", func() { m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) m.Post("/access_mode", repo.ChangeCollaborationAccessMode) @@ -923,7 +934,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/", lfs.PostLockHandler) m.Post("/verify", lfs.VerifyLockHandler) m.Post("/:lid/unlock", lfs.UnLockHandler) - }, context.RepoAssignment()) + }) m.Any("/*", func(ctx *context.Context) { ctx.NotFound("", nil) }) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index 2612f70a67178..1351ca040b7fa 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -359,11 +359,11 @@ func RegisterOpenIDPost(ctx *context.Context, cpt *captcha.Captcha, form auth.Si } } - len := setting.MinPasswordLength - if len < 256 { - len = 256 + length := setting.MinPasswordLength + if length < 256 { + length = 256 } - password, err := generate.GetRandomString(len) + password, err := generate.GetRandomString(length) if err != nil { ctx.RenderWithErr(err.Error(), tplSignUpOID, form) return diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index 85c9c83fd19af..ac5c4c97fb27a 100644 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -127,6 +127,10 @@ func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *mo } defer fr.Close() + if form.Avatar.Size > setting.AvatarMaxFileSize { + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + } + data, err := ioutil.ReadAll(fr) if err != nil { return fmt.Errorf("ioutil.ReadAll: %v", err) diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go index 3a590f0b08862..fca1151a04fc9 100644 --- a/routers/user/setting/security_twofa.go +++ b/routers/user/setting/security_twofa.go @@ -74,11 +74,13 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool { if uri != nil { otpKey, err = otp.NewKeyFromURL(uri.(string)) } + // Filter unsafe character ':' in issuer + issuer := strings.Replace(setting.AppName+" ("+setting.Domain+")", ":", "", -1) if otpKey == nil { err = nil // clear the error, in case the URL was invalid otpKey, err = totp.Generate(totp.GenerateOpts{ SecretSize: 40, - Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")", + Issuer: issuer, AccountName: ctx.User.Name, }) if err != nil { diff --git a/templates/admin/dashboard.tmpl b/templates/admin/dashboard.tmpl index 13c06334a5e45..262db04b90608 100644 --- a/templates/admin/dashboard.tmpl +++ b/templates/admin/dashboard.tmpl @@ -53,6 +53,10 @@ {{.i18n.Tr "admin.dashboard.git_fsck"}} {{.i18n.Tr "admin.dashboard.operation_run"}} + + {{.i18n.Tr "admin.dashboard.delete_generated_repository_avatars"}} + {{.i18n.Tr "admin.dashboard.operation_run"}} + diff --git a/templates/explore/repo_list.tmpl b/templates/explore/repo_list.tmpl index b1768170010d2..8c7ba51a54005 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/explore/repo_list.tmpl @@ -2,6 +2,9 @@ {{range .Repos}}
- {{if .DescriptionHTML}}

{{.DescriptionHTML}}

{{end}} - {{if .Topics }} -
- {{range .Topics}} - {{if ne . "" }}
{{.}}
{{end}} +
+ {{if .DescriptionHTML}}

{{.DescriptionHTML}}

{{end}} + {{if .Topics }} +
+ {{range .Topics}} + {{if ne . "" }}
{{.}}
{{end}} + {{end}} +
{{end}} -
- {{end}} -

{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}

+

{{$.i18n.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.i18n.Lang}}

+
{{else}}
diff --git a/templates/home.tmpl b/templates/home.tmpl index 3ab8fc44e980c..fa48cdc1b60ba 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -382,7 +382,10 @@ Open Source

- It's all on GitHub! Join us by contributing to make this project even better. Don't be shy to be a contributor! +Go get code.gitea.io/gitea! +Join us by contributing +to make this project even better. +Don't be shy to be a contributor!

diff --git a/templates/install.tmpl b/templates/install.tmpl index f45052ccd07ff..f8d1ef04e4e21 100644 --- a/templates/install.tmpl +++ b/templates/install.tmpl @@ -28,7 +28,7 @@ -
+
@@ -64,6 +64,21 @@
+
+
+ + +
+
+
diff --git a/templates/repo/diff/page.tmpl b/templates/repo/diff/page.tmpl index c8f5a3d9f0efc..c35e2a415b4b9 100644 --- a/templates/repo/diff/page.tmpl +++ b/templates/repo/diff/page.tmpl @@ -65,6 +65,27 @@
{{end}} {{end}} + {{if .Note}} +
+ + {{.i18n.Tr "repo.diff.git-notes"}}: + {{if .NoteAuthor}} + + {{if .NoteAuthor.FullName}} + {{.NoteAuthor.FullName}} + {{else}} + {{.NoteCommit.Author.Name}} + {{end}} + + {{else}} + {{.NoteCommit.Author.Name}} + {{end}} + {{TimeSince .NoteCommit.Author.When $.Lang}} +
+
+
{{RenderNote .Note $.RepoLink $.Repository.ComposeMetas}}
+
+ {{end}} {{end}} {{template "repo/diff/box" .}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index d340575353209..f4eefd3fde1d5 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -3,7 +3,11 @@
+ +
+ +
+ {{.CsrfTokenHtml}} +
+ + +
+ +
+ + {{$.i18n.Tr "settings.delete_current_avatar"}} +
+
+
{{if .Repository.IsMirror}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c0790ac23edff..77515bb139e61 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -552,6 +552,7 @@ { "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/EditOrgOption" } @@ -1210,6 +1211,51 @@ "$ref": "#/responses/forbidden" } } + }, + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Edit a repository's properties. Only fields that are set will be changed.", + "operationId": "repoEdit", + "parameters": [ + { + "type": "string", + "description": "owner of the repo to edit", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo to edit", + "name": "repo", + "in": "path", + "required": true + }, + { + "description": "Properties of a repo that you can edit", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/EditRepoOption" + } + } + ], + "responses": { + "200": { + "$ref": "#/responses/Repository" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "422": { + "$ref": "#/responses/validationError" + } + } } }, "/repos/{owner}/{repo}/archive/{archive}": { @@ -1604,9 +1650,9 @@ "required": true }, { - "description": "'content' must be base64 encoded\n\n 'sha' is the SHA for the file that already exists\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before updating the file", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/UpdateFileOptions" } @@ -1653,9 +1699,9 @@ "required": true }, { - "description": "'content' must be base64 encoded\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'sha' is the SHA for the file that already exists\n\n 'new_branch' (optional) will make a new branch from 'branch' before creating the file", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/CreateFileOptions" } @@ -1702,9 +1748,9 @@ "required": true }, { - "description": "'sha' is the SHA for the file to be deleted\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before deleting the file", "name": "body", "in": "body", + "required": true, "schema": { "$ref": "#/definitions/DeleteFileOptions" } @@ -6037,6 +6083,12 @@ "responses": { "201": { "$ref": "#/responses/Repository" + }, + "409": { + "description": "The repository with the same name already exists." + }, + "422": { + "$ref": "#/responses/validationError" } } } @@ -6884,13 +6936,17 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "CreateFileOptions": { - "description": "CreateFileOptions options for creating files", + "description": "CreateFileOptions options for creating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", + "required": [ + "content" + ], "properties": { "author": { "$ref": "#/definitions/Identity" }, "branch": { + "description": "branch (optional) to base this file from. if not given, the default branch is used", "type": "string", "x-go-name": "BranchName" }, @@ -6898,14 +6954,17 @@ "$ref": "#/definitions/Identity" }, "content": { + "description": "content must be base64 encoded", "type": "string", "x-go-name": "Content" }, "message": { + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", "type": "string", "x-go-name": "Message" }, "new_branch": { + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", "type": "string", "x-go-name": "NewBranchName" } @@ -7140,7 +7199,14 @@ "x-go-name": "UserName" }, "visibility": { - "$ref": "#/definitions/VisibleType" + "description": "possible values are `public` (default), `limited` or `private`", + "type": "string", + "enum": [ + "public", + "limited", + "private" + ], + "x-go-name": "Visibility" }, "website": { "type": "string", @@ -7408,13 +7474,17 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "DeleteFileOptions": { - "description": "DeleteFileOptions options for deleting files (used for other File structs below)", + "description": "DeleteFileOptions options for deleting files (used for other File structs below)\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", + "required": [ + "sha" + ], "properties": { "author": { "$ref": "#/definitions/Identity" }, "branch": { + "description": "branch (optional) to base this file from. if not given, the default branch is used", "type": "string", "x-go-name": "BranchName" }, @@ -7422,14 +7492,17 @@ "$ref": "#/definitions/Identity" }, "message": { + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", "type": "string", "x-go-name": "Message" }, "new_branch": { + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", "type": "string", "x-go-name": "NewBranchName" }, "sha": { + "description": "sha is the SHA for the file that already exists", "type": "string", "x-go-name": "SHA" } @@ -7652,6 +7725,16 @@ "type": "string", "x-go-name": "Location" }, + "visibility": { + "description": "possible values are `public`, `limited` or `private`", + "type": "string", + "enum": [ + "public", + "limited", + "private" + ], + "x-go-name": "Visibility" + }, "website": { "type": "string", "x-go-name": "Website" @@ -7738,6 +7821,84 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "EditRepoOption": { + "description": "EditRepoOption options when editing a repository's properties", + "type": "object", + "properties": { + "allow_merge_commits": { + "description": "either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "description": "either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_explicit": { + "description": "either `true` to allow rebase with explicit merge commits (--no-ff), or `false` to prevent rebase with explicit merge commits. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash_merge": { + "description": "either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "AllowSquash" + }, + "archived": { + "description": "set to `true` to archive this repository.", + "type": "boolean", + "x-go-name": "Archived" + }, + "default_branch": { + "description": "sets the default branch for this repository.", + "type": "string", + "x-go-name": "DefaultBranch" + }, + "description": { + "description": "a short description of the repository.", + "type": "string", + "x-go-name": "Description" + }, + "has_issues": { + "description": "either `true` to enable issues for this repository or `false` to disable them.", + "type": "boolean", + "x-go-name": "HasIssues" + }, + "has_pull_requests": { + "description": "either `true` to allow pull requests, or `false` to prevent pull request.", + "type": "boolean", + "x-go-name": "HasPullRequests" + }, + "has_wiki": { + "description": "either `true` to enable the wiki for this repository or `false` to disable it.", + "type": "boolean", + "x-go-name": "HasWiki" + }, + "ignore_whitespace_conflicts": { + "description": "either `true` to ignore whitespace for conflicts, or `false` to not ignore whitespace. `has_pull_requests` must be `true`.", + "type": "boolean", + "x-go-name": "IgnoreWhitespaceConflicts" + }, + "name": { + "description": "name of the repository", + "type": "string", + "uniqueItems": true, + "x-go-name": "Name" + }, + "private": { + "description": "either `true` to make the repository private or `false` to make it public.\nNote: you will get a 422 error if the organization restricts changing repository visibility to organization\nowners and a non-owner tries to change the value of private.", + "type": "boolean", + "x-go-name": "Private" + }, + "website": { + "description": "a URL with more information about the repository.", + "type": "string", + "x-go-name": "Website" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "EditTeamOption": { "description": "EditTeamOption options for editing a team", "type": "object", @@ -8612,7 +8773,8 @@ "x-go-name": "UserName" }, "visibility": { - "$ref": "#/definitions/VisibleType" + "type": "string", + "x-go-name": "Visibility" }, "website": { "type": "string", @@ -9062,10 +9224,30 @@ "description": "Repository represents a repository", "type": "object", "properties": { + "allow_merge_commits": { + "type": "boolean", + "x-go-name": "AllowMerge" + }, + "allow_rebase": { + "type": "boolean", + "x-go-name": "AllowRebase" + }, + "allow_rebase_explicit": { + "type": "boolean", + "x-go-name": "AllowRebaseMerge" + }, + "allow_squash_merge": { + "type": "boolean", + "x-go-name": "AllowSquash" + }, "archived": { "type": "boolean", "x-go-name": "Archived" }, + "avatar_url": { + "type": "string", + "x-go-name": "AvatarURL" + }, "clone_url": { "type": "string", "x-go-name": "CloneURL" @@ -9100,6 +9282,18 @@ "type": "string", "x-go-name": "FullName" }, + "has_issues": { + "type": "boolean", + "x-go-name": "HasIssues" + }, + "has_pull_requests": { + "type": "boolean", + "x-go-name": "HasPullRequests" + }, + "has_wiki": { + "type": "boolean", + "x-go-name": "HasWiki" + }, "html_url": { "type": "string", "x-go-name": "HTMLURL" @@ -9109,6 +9303,10 @@ "format": "int64", "x-go-name": "ID" }, + "ignore_whitespace_conflicts": { + "type": "boolean", + "x-go-name": "IgnoreWhitespaceConflicts" + }, "mirror": { "type": "boolean", "x-go-name": "Mirror" @@ -9372,13 +9570,18 @@ "x-go-package": "code.gitea.io/gitea/modules/structs" }, "UpdateFileOptions": { - "description": "UpdateFileOptions options for updating files", + "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", "type": "object", + "required": [ + "sha", + "content" + ], "properties": { "author": { "$ref": "#/definitions/Identity" }, "branch": { + "description": "branch (optional) to base this file from. if not given, the default branch is used", "type": "string", "x-go-name": "BranchName" }, @@ -9386,22 +9589,27 @@ "$ref": "#/definitions/Identity" }, "content": { + "description": "content must be base64 encoded", "type": "string", "x-go-name": "Content" }, "from_path": { + "description": "from_path (optional) is the path of the original file which will be moved/renamed to the path in the URL", "type": "string", "x-go-name": "FromPath" }, "message": { + "description": "message (optional) for the commit of this file. if not supplied, a default message will be used", "type": "string", "x-go-name": "Message" }, "new_branch": { + "description": "new_branch (optional) will make a new branch from `branch` before creating the file", "type": "string", "x-go-name": "NewBranchName" }, "sha": { + "description": "sha is the SHA for the file that already exists", "type": "string", "x-go-name": "SHA" } @@ -9466,12 +9674,6 @@ }, "x-go-package": "code.gitea.io/gitea/models" }, - "VisibleType": { - "description": "VisibleType defines the visibility (Organization only)", - "type": "integer", - "format": "int64", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "WatchInfo": { "description": "WatchInfo represents an API watch status of one repository", "type": "object", diff --git a/vendor/github.com/oliamb/cutter/.gitignore b/vendor/github.com/oliamb/cutter/.gitignore new file mode 100644 index 0000000000000..00268614f0456 --- /dev/null +++ b/vendor/github.com/oliamb/cutter/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/oliamb/cutter/.travis.yml b/vendor/github.com/oliamb/cutter/.travis.yml new file mode 100644 index 0000000000000..70e012b81e44d --- /dev/null +++ b/vendor/github.com/oliamb/cutter/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - 1.0 + - 1.1 + - tip diff --git a/vendor/github.com/oliamb/cutter/LICENSE b/vendor/github.com/oliamb/cutter/LICENSE new file mode 100644 index 0000000000000..5412782c6e9a6 --- /dev/null +++ b/vendor/github.com/oliamb/cutter/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Olivier Amblet + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/oliamb/cutter/README.md b/vendor/github.com/oliamb/cutter/README.md new file mode 100644 index 0000000000000..b54f9e3616c43 --- /dev/null +++ b/vendor/github.com/oliamb/cutter/README.md @@ -0,0 +1,107 @@ +Cutter +====== + +A Go library to crop images. + +[![Build Status](https://travis-ci.org/oliamb/cutter.png?branch=master)](https://travis-ci.org/oliamb/cutter) +[![GoDoc](https://godoc.org/github.com/oliamb/cutter?status.png)](https://godoc.org/github.com/oliamb/cutter) + +Cutter was initially developped to be able +to crop image resized using github.com/nfnt/resize. + +Usage +----- + +Read the doc on https://godoc.org/github.com/oliamb/cutter + +Import package with + +```go +import "github.com/oliamb/cutter" +``` + +Package cutter provides a function to crop image. + +By default, the original image will be cropped at the +given size from the top left corner. + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, +}) +``` + +Most of the time, the cropped image will share some memory +with the original, so it should be used read only. You must +ask explicitely for a copy if nedded. + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Options: cutter.Copy, +}) +``` + +It is possible to specify the top left position: + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Anchor: image.Point{100, 100}, + Mode: cutter.TopLeft, // optional, default value +}) +``` + +The Anchor property can represents the center of the cropped image +instead of the top left corner: + +```go +croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Mode: cutter.Centered, +}) +``` + +The default crop use the specified dimension, but it is possible +to use Width and Heigth as a ratio instead. In this case, +the resulting image will be as big as possible to fit the asked ratio +from the anchor position. + +```go +croppedImg, err := cutter.Crop(baseImage, cutter.Config{ + Width: 4, + Height: 3, + Mode: cutter.Centered, + Options: cutter.Ratio&cutter.Copy, // Copy is useless here +}) +``` + +About resize +------------ +This lib only manage crop and won't resize image, but it works great in combination with [github.com/nfnt/resize](https://github.com/nfnt/resize) + +Contributing +------------ +I'd love to see your contributions to Cutter. If you'd like to hack on it: + +- fork the project, +- hack on it, +- ensure tests pass, +- make a pull request + +If you plan to modify the API, let's disscuss it first. + +Licensing +--------- +MIT License, Please see the file called LICENSE. + +Credits +------- +Test Picture: Gopher picture from Heidi Schuyt, http://www.flickr.com/photos/hschuyt/7674222278/, +© copyright Creative Commons(http://creativecommons.org/licenses/by-nc-sa/2.0/) + +Thanks to Urturn(http://www.urturn.com) for the time allocated to develop the library. diff --git a/vendor/github.com/oliamb/cutter/cutter.go b/vendor/github.com/oliamb/cutter/cutter.go new file mode 100644 index 0000000000000..29d9d2f75882a --- /dev/null +++ b/vendor/github.com/oliamb/cutter/cutter.go @@ -0,0 +1,192 @@ +/* +Package cutter provides a function to crop image. + +By default, the original image will be cropped at the +given size from the top left corner. + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + }) + +Most of the time, the cropped image will share some memory +with the original, so it should be used read only. You must +ask explicitely for a copy if nedded. + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Options: Copy, + }) + +It is possible to specify the top left position: + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Anchor: image.Point{100, 100}, + Mode: TopLeft, // optional, default value + }) + +The Anchor property can represents the center of the cropped image +instead of the top left corner: + + + croppedImg, err := cutter.Crop(img, cutter.Config{ + Width: 250, + Height: 500, + Mode: Centered, + }) + +The default crop use the specified dimension, but it is possible +to use Width and Heigth as a ratio instead. In this case, +the resulting image will be as big as possible to fit the asked ratio +from the anchor position. + + croppedImg, err := cutter.Crop(baseImage, cutter.Config{ + Width: 4, + Height: 3, + Mode: Centered, + Options: Ratio, + }) +*/ +package cutter + +import ( + "image" + "image/draw" +) + +// Config is used to defined +// the way the crop should be realized. +type Config struct { + Width, Height int + Anchor image.Point // The Anchor Point in the source image + Mode AnchorMode // Which point in the resulting image the Anchor Point is referring to + Options Option +} + +// AnchorMode is an enumeration of the position an anchor can represent. +type AnchorMode int + +const ( + // TopLeft defines the Anchor Point + // as the top left of the cropped picture. + TopLeft AnchorMode = iota + // Centered defines the Anchor Point + // as the center of the cropped picture. + Centered = iota +) + +// Option flags to modify the way the crop is done. +type Option int + +const ( + // Ratio flag is use when Width and Height + // must be used to compute a ratio rather + // than absolute size in pixels. + Ratio Option = 1 << iota + // Copy flag is used to enforce the function + // to retrieve a copy of the selected pixels. + // This disable the use of SubImage method + // to compute the result. + Copy = 1 << iota +) + +// An interface that is +// image.Image + SubImage method. +type subImageSupported interface { + SubImage(r image.Rectangle) image.Image +} + +// Crop retrieves an image that is a +// cropped copy of the original img. +// +// The crop is made given the informations provided in config. +func Crop(img image.Image, c Config) (image.Image, error) { + maxBounds := c.maxBounds(img.Bounds()) + size := c.computeSize(maxBounds, image.Point{c.Width, c.Height}) + cr := c.computedCropArea(img.Bounds(), size) + cr = img.Bounds().Intersect(cr) + + if c.Options&Copy == Copy { + return cropWithCopy(img, cr) + } + if dImg, ok := img.(subImageSupported); ok { + return dImg.SubImage(cr), nil + } + return cropWithCopy(img, cr) +} + +func cropWithCopy(img image.Image, cr image.Rectangle) (image.Image, error) { + result := image.NewRGBA(cr) + draw.Draw(result, cr, img, cr.Min, draw.Src) + return result, nil +} + +func (c Config) maxBounds(bounds image.Rectangle) (r image.Rectangle) { + if c.Mode == Centered { + anchor := c.centeredMin(bounds) + w := min(anchor.X-bounds.Min.X, bounds.Max.X-anchor.X) + h := min(anchor.Y-bounds.Min.Y, bounds.Max.Y-anchor.Y) + r = image.Rect(anchor.X-w, anchor.Y-h, anchor.X+w, anchor.Y+h) + } else { + r = image.Rect(c.Anchor.X, c.Anchor.Y, bounds.Max.X, bounds.Max.Y) + } + return +} + +// computeSize retrieve the effective size of the cropped image. +// It is defined by Height, Width, and Ratio option. +func (c Config) computeSize(bounds image.Rectangle, ratio image.Point) (p image.Point) { + if c.Options&Ratio == Ratio { + // Ratio option is on, so we take the biggest size available that fit the given ratio. + if float64(ratio.X)/float64(bounds.Dx()) > float64(ratio.Y)/float64(bounds.Dy()) { + p = image.Point{bounds.Dx(), (bounds.Dx() / ratio.X) * ratio.Y} + } else { + p = image.Point{(bounds.Dy() / ratio.Y) * ratio.X, bounds.Dy()} + } + } else { + p = image.Point{ratio.X, ratio.Y} + } + return +} + +// computedCropArea retrieve the theorical crop area. +// It is defined by Height, Width, Mode and +func (c Config) computedCropArea(bounds image.Rectangle, size image.Point) (r image.Rectangle) { + min := bounds.Min + switch c.Mode { + case Centered: + rMin := c.centeredMin(bounds) + r = image.Rect(rMin.X-size.X/2, rMin.Y-size.Y/2, rMin.X-size.X/2+size.X, rMin.Y-size.Y/2+size.Y) + default: // TopLeft + rMin := image.Point{min.X + c.Anchor.X, min.Y + c.Anchor.Y} + r = image.Rect(rMin.X, rMin.Y, rMin.X+size.X, rMin.Y+size.Y) + } + return +} + +func (c *Config) centeredMin(bounds image.Rectangle) (rMin image.Point) { + if c.Anchor.X == 0 && c.Anchor.Y == 0 { + rMin = image.Point{ + X: bounds.Dx() / 2, + Y: bounds.Dy() / 2, + } + } else { + rMin = image.Point{ + X: c.Anchor.X, + Y: c.Anchor.Y, + } + } + return +} + +func min(a, b int) (r int) { + if a < b { + r = a + } else { + r = b + } + return +} diff --git a/vendor/gopkg.in/src-d/go-git.v4/.travis.yml b/vendor/gopkg.in/src-d/go-git.v4/.travis.yml index c68b5f473e0aa..3a65f3e08212e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/.travis.yml +++ b/vendor/gopkg.in/src-d/go-git.v4/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - "1.10" - "1.11" + - "1.12" go_import_path: gopkg.in/src-d/go-git.v4 diff --git a/vendor/gopkg.in/src-d/go-git.v4/options.go b/vendor/gopkg.in/src-d/go-git.v4/options.go index ed7689ab3f894..7c9e68728827d 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/options.go +++ b/vendor/gopkg.in/src-d/go-git.v4/options.go @@ -229,7 +229,7 @@ var ( ErrCreateRequiresBranch = errors.New("Branch is mandatory when Create is used") ) -// CheckoutOptions describes how a checkout 31operation should be performed. +// CheckoutOptions describes how a checkout operation should be performed. type CheckoutOptions struct { // Hash is the hash of the commit to be checked out. If used, HEAD will be // in detached mode. If Create is not used, Branch and Hash are mutually @@ -288,7 +288,7 @@ const ( // ResetOptions describes how a reset operation should be performed. type ResetOptions struct { - // Commit, if commit is pressent set the current branch head (HEAD) to it. + // Commit, if commit is present set the current branch head (HEAD) to it. Commit plumbing.Hash // Mode, form resets the current branch head to Commit and possibly updates // the index (resetting it to the tree of Commit) and the working tree diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go index 53d8b02d961c7..cd3712b7d7b39 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/cache/object_lru.go @@ -61,6 +61,11 @@ func (c *ObjectLRU) Put(obj plumbing.EncodedObject) { c.actualSize += objSize for c.actualSize > c.MaxSize { last := c.ll.Back() + if last == nil { + c.actualSize = 0 + break + } + lastObj := last.Value.(plumbing.EncodedObject) lastSize := FileSize(lastObj.Size()) diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go index e2543426ac5a4..b569d3ce2db1d 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit.go @@ -76,8 +76,8 @@ func (c *Commit) Tree() (*Tree, error) { return GetTree(c.s, c.TreeHash) } -// Patch returns the Patch between the actual commit and the provided one. -// Error will be return if context expires. Provided context must be non-nil +// PatchContext returns the Patch between the actual commit and the provided one. +// Error will be return if context expires. Provided context must be non-nil. func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) { fromTree, err := c.Tree() if err != nil { @@ -291,25 +291,33 @@ func (b *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { return err } -// Stats shows the status of commit. +// Stats returns the stats of a commit. func (c *Commit) Stats() (FileStats, error) { - // Get the previous commit. - ci := c.Parents() - parentCommit, err := ci.Next() + return c.StatsContext(context.Background()) +} + +// StatsContext returns the stats of a commit. Error will be return if context +// expires. Provided context must be non-nil. +func (c *Commit) StatsContext(ctx context.Context) (FileStats, error) { + fromTree, err := c.Tree() if err != nil { - if err == io.EOF { - emptyNoder := treeNoder{} - parentCommit = &Commit{ - Hash: emptyNoder.hash, - // TreeHash: emptyNoder.parent.Hash, - s: c.s, - } - } else { + return nil, err + } + + toTree := &Tree{} + if c.NumParents() != 0 { + firstParent, err := c.Parents().Next() + if err != nil { + return nil, err + } + + toTree, err = firstParent.Tree() + if err != nil { return nil, err } } - patch, err := parentCommit.Patch(c) + patch, err := toTree.PatchContext(ctx, fromTree) if err != nil { return nil, err } diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go index aef1cf24c6990..dabfe75c27b8e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/commit_walker_bfs.go @@ -67,7 +67,7 @@ func (w *bfsCommitIterator) Next() (*Commit, error) { for _, h := range c.ParentHashes { err := w.appendHash(c.s, h) if err != nil { - return nil, nil + return nil, err } } diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go index adeaccb0a86a6..068589eff8e1e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go @@ -320,11 +320,18 @@ func getFileStatsFromFilePatches(filePatches []fdiff.FilePatch) FileStats { } for _, chunk := range fp.Chunks() { + s := chunk.Content() switch chunk.Type() { case fdiff.Add: - cs.Addition += strings.Count(chunk.Content(), "\n") + cs.Addition += strings.Count(s, "\n") + if s[len(s)-1] != '\n' { + cs.Addition++ + } case fdiff.Delete: - cs.Deletion += strings.Count(chunk.Content(), "\n") + cs.Deletion += strings.Count(s, "\n") + if s[len(s)-1] != '\n' { + cs.Deletion++ + } } } diff --git a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go index 78d61a1fba546..1f9ea2651c517 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go +++ b/vendor/gopkg.in/src-d/go-git.v4/plumbing/object/tree.go @@ -135,7 +135,7 @@ func (t *Tree) FindEntry(path string) (*TreeEntry, error) { pathCurrent := "" // search for the longest path in the tree path cache - for i := len(pathParts); i > 1; i-- { + for i := len(pathParts) - 1; i > 1; i-- { path := filepath.Join(pathParts[:i]...) tree, ok := t.t[path] diff --git a/vendor/gopkg.in/src-d/go-git.v4/remote.go b/vendor/gopkg.in/src-d/go-git.v4/remote.go index de537ce8e8bcd..80604092ab17e 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/remote.go +++ b/vendor/gopkg.in/src-d/go-git.v4/remote.go @@ -1020,7 +1020,12 @@ func pushHashes( if err != nil { return nil, err } - done := make(chan error) + + // Set buffer size to 1 so the error message can be written when + // ReceivePack fails. Otherwise the goroutine will be blocked writing + // to the channel. + done := make(chan error, 1) + go func() { e := packfile.NewEncoder(wr, s, useRefDeltas) if _, err := e.Encode(hs, config.Pack.Window); err != nil { @@ -1033,6 +1038,8 @@ func pushHashes( rs, err := sess.ReceivePack(ctx, req) if err != nil { + // close the pipe to unlock encode write + _ = rd.Close() return nil, err } diff --git a/vendor/gopkg.in/src-d/go-git.v4/repository.go b/vendor/gopkg.in/src-d/go-git.v4/repository.go index de92d647099df..e5b12b0c52f17 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/repository.go +++ b/vendor/gopkg.in/src-d/go-git.v4/repository.go @@ -49,6 +49,7 @@ var ( ErrRepositoryAlreadyExists = errors.New("repository already exists") ErrRemoteNotFound = errors.New("remote not found") ErrRemoteExists = errors.New("remote already exists") + ErrAnonymousRemoteName = errors.New("anonymous remote name must be 'anonymous'") ErrWorktreeNotProvided = errors.New("worktree should be provided") ErrIsBareRepository = errors.New("worktree not available in a bare repository") ErrUnableToResolveCommit = errors.New("unable to resolve commit") @@ -492,6 +493,22 @@ func (r *Repository) CreateRemote(c *config.RemoteConfig) (*Remote, error) { return remote, r.Storer.SetConfig(cfg) } +// CreateRemoteAnonymous creates a new anonymous remote. c.Name must be "anonymous". +// It's used like 'git fetch git@github.com:src-d/go-git.git master:master'. +func (r *Repository) CreateRemoteAnonymous(c *config.RemoteConfig) (*Remote, error) { + if err := c.Validate(); err != nil { + return nil, err + } + + if c.Name != "anonymous" { + return nil, ErrAnonymousRemoteName + } + + remote := newRemote(r.Storer, c) + + return remote, nil +} + // DeleteRemote delete a remote from the repository and delete the config func (r *Repository) DeleteRemote(name string) error { cfg, err := r.Storer.Config() diff --git a/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go b/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go index f49ae55baeb4a..6142ed051550a 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go +++ b/vendor/gopkg.in/src-d/go-git.v4/utils/diff/diff.go @@ -8,14 +8,30 @@ package diff import ( "bytes" + "time" "github.com/sergi/go-diff/diffmatchpatch" ) // Do computes the (line oriented) modifications needed to turn the src -// string into the dst string. +// string into the dst string. The underlying algorithm is Meyers, +// its complexity is O(N*d) where N is min(lines(src), lines(dst)) and d +// is the size of the diff. func Do(src, dst string) (diffs []diffmatchpatch.Diff) { + // the default timeout is time.Second which may be too small under heavy load + return DoWithTimeout(src, dst, time.Hour) +} + +// DoWithTimeout computes the (line oriented) modifications needed to turn the src +// string into the dst string. The `timeout` argument specifies the maximum +// amount of time it is allowed to spend in this function. If the timeout +// is exceeded, the parts of the strings which were not considered are turned into +// a bulk delete+insert and the half-baked suboptimal result is returned at once. +// The underlying algorithm is Meyers, its complexity is O(N*d) where N is +// min(lines(src), lines(dst)) and d is the size of the diff. +func DoWithTimeout (src, dst string, timeout time.Duration) (diffs []diffmatchpatch.Diff) { dmp := diffmatchpatch.New() + dmp.DiffTimeout = timeout wSrc, wDst, warray := dmp.DiffLinesToRunes(src, dst) diffs = dmp.DiffMainRunes(wSrc, wDst, false) diffs = dmp.DiffCharsToLines(diffs, warray) diff --git a/vendor/gopkg.in/src-d/go-git.v4/worktree.go b/vendor/gopkg.in/src-d/go-git.v4/worktree.go index a14fd8d6c34e5..dae40a38a7163 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/worktree.go +++ b/vendor/gopkg.in/src-d/go-git.v4/worktree.go @@ -152,17 +152,6 @@ func (w *Worktree) Checkout(opts *CheckoutOptions) error { } } - if !opts.Force { - unstaged, err := w.containsUnstagedChanges() - if err != nil { - return err - } - - if unstaged { - return ErrUnstagedChanges - } - } - c, err := w.getCommitFromCheckoutOptions(opts) if err != nil { return err diff --git a/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go b/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go index 0e113d0937632..16ce937077c82 100644 --- a/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go +++ b/vendor/gopkg.in/src-d/go-git.v4/worktree_status.go @@ -142,12 +142,16 @@ func (w *Worktree) diffStagingWithWorktree(reverse bool) (merkletrie.Changes, er func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.Changes { patterns, err := gitignore.ReadPatterns(w.Filesystem, nil) - if err != nil || len(patterns) == 0 { + if err != nil { return changes } patterns = append(patterns, w.Excludes...) + if len(patterns) == 0 { + return changes + } + m := gitignore.NewMatcher(patterns) var res merkletrie.Changes diff --git a/vendor/modules.txt b/vendor/modules.txt index 0013ea356f6dc..9f9ae9b4fbe66 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -261,6 +261,8 @@ github.com/mschoch/smat github.com/msteinert/pam # github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 github.com/nfnt/resize +# github.com/oliamb/cutter v0.2.2 +github.com/oliamb/cutter # github.com/pelletier/go-buffruneio v0.2.0 github.com/pelletier/go-buffruneio # github.com/philhofer/fwd v1.0.0 @@ -420,7 +422,7 @@ gopkg.in/src-d/go-billy.v4 gopkg.in/src-d/go-billy.v4/util gopkg.in/src-d/go-billy.v4/helper/chroot gopkg.in/src-d/go-billy.v4/helper/polyfill -# gopkg.in/src-d/go-git.v4 v4.10.0 +# gopkg.in/src-d/go-git.v4 v4.11.0 gopkg.in/src-d/go-git.v4 gopkg.in/src-d/go-git.v4/config gopkg.in/src-d/go-git.v4/plumbing