aboutsummaryrefslogtreecommitdiff
path: root/internal/cmd
diff options
context:
space:
mode:
authorUnknwon <u@gogs.io>2019-10-24 01:51:46 -0700
committerGitHub <noreply@github.com>2019-10-24 01:51:46 -0700
commit01c8df01ec0608f1f25b2f1444adabb98fa5ee8a (patch)
treef8a7e5dd8d2a8c51e1ce2cabb9d33571a93314dd /internal/cmd
parent613139e7bef81d3573e7988a47eb6765f3de347a (diff)
internal: move packages under this directory (#5836)
* Rename pkg -> internal * Rename routes -> route * Move route -> internal/route * Rename models -> db * Move db -> internal/db * Fix route2 -> route * Move cmd -> internal/cmd * Bump version
Diffstat (limited to 'internal/cmd')
-rw-r--r--internal/cmd/admin.go183
-rw-r--r--internal/cmd/backup.go137
-rw-r--r--internal/cmd/cert.go163
-rw-r--r--internal/cmd/cert_stub.go28
-rw-r--r--internal/cmd/cmd.go42
-rw-r--r--internal/cmd/hook.go277
-rw-r--r--internal/cmd/import.go113
-rw-r--r--internal/cmd/restore.go148
-rw-r--r--internal/cmd/serv.go272
-rw-r--r--internal/cmd/web.go757
10 files changed, 2120 insertions, 0 deletions
diff --git a/internal/cmd/admin.go b/internal/cmd/admin.go
new file mode 100644
index 00000000..de8b6ba6
--- /dev/null
+++ b/internal/cmd/admin.go
@@ -0,0 +1,183 @@
+// Copyright 2016 The Gogs 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 cmd
+
+import (
+ "fmt"
+ "reflect"
+ "runtime"
+
+ "github.com/urfave/cli"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/setting"
+)
+
+var (
+ Admin = cli.Command{
+ Name: "admin",
+ Usage: "Perform admin operations on command line",
+ Description: `Allow using internal logic of Gogs without hacking into the source code
+to make automatic initialization process more smoothly`,
+ Subcommands: []cli.Command{
+ subcmdCreateUser,
+ subcmdDeleteInactivateUsers,
+ subcmdDeleteRepositoryArchives,
+ subcmdDeleteMissingRepositories,
+ subcmdGitGcRepos,
+ subcmdRewriteAuthorizedKeys,
+ subcmdSyncRepositoryHooks,
+ subcmdReinitMissingRepositories,
+ },
+ }
+
+ subcmdCreateUser = cli.Command{
+ Name: "create-user",
+ Usage: "Create a new user in database",
+ Action: runCreateUser,
+ Flags: []cli.Flag{
+ stringFlag("name", "", "Username"),
+ stringFlag("password", "", "User password"),
+ stringFlag("email", "", "User email address"),
+ boolFlag("admin", "User is an admin"),
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdDeleteInactivateUsers = cli.Command{
+ Name: "delete-inactive-users",
+ Usage: "Delete all inactive accounts",
+ Action: adminDashboardOperation(
+ db.DeleteInactivateUsers,
+ "All inactivate accounts have been deleted successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdDeleteRepositoryArchives = cli.Command{
+ Name: "delete-repository-archives",
+ Usage: "Delete all repositories archives",
+ Action: adminDashboardOperation(
+ db.DeleteRepositoryArchives,
+ "All repositories archives have been deleted successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdDeleteMissingRepositories = cli.Command{
+ Name: "delete-missing-repositories",
+ Usage: "Delete all repository records that lost Git files",
+ Action: adminDashboardOperation(
+ db.DeleteMissingRepositories,
+ "All repositories archives have been deleted successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdGitGcRepos = cli.Command{
+ Name: "collect-garbage",
+ Usage: "Do garbage collection on repositories",
+ Action: adminDashboardOperation(
+ db.GitGcRepos,
+ "All repositories have done garbage collection successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdRewriteAuthorizedKeys = cli.Command{
+ Name: "rewrite-authorized-keys",
+ Usage: "Rewrite '.ssh/authorized_keys' file (caution: non-Gogs keys will be lost)",
+ Action: adminDashboardOperation(
+ db.RewriteAuthorizedKeys,
+ "All public keys have been rewritten successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdSyncRepositoryHooks = cli.Command{
+ Name: "resync-hooks",
+ Usage: "Resync pre-receive, update and post-receive hooks",
+ Action: adminDashboardOperation(
+ db.SyncRepositoryHooks,
+ "All repositories' pre-receive, update and post-receive hooks have been resynced successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+
+ subcmdReinitMissingRepositories = cli.Command{
+ Name: "reinit-missing-repositories",
+ Usage: "Reinitialize all repository records that lost Git files",
+ Action: adminDashboardOperation(
+ db.ReinitMissingRepositories,
+ "All repository records that lost Git files have been reinitialized successfully",
+ ),
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+)
+
+func runCreateUser(c *cli.Context) error {
+ if !c.IsSet("name") {
+ return fmt.Errorf("Username is not specified")
+ } else if !c.IsSet("password") {
+ return fmt.Errorf("Password is not specified")
+ } else if !c.IsSet("email") {
+ return fmt.Errorf("Email is not specified")
+ }
+
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+
+ setting.NewContext()
+ db.LoadConfigs()
+ db.SetEngine()
+
+ if err := db.CreateUser(&db.User{
+ Name: c.String("name"),
+ Email: c.String("email"),
+ Passwd: c.String("password"),
+ IsActive: true,
+ IsAdmin: c.Bool("admin"),
+ }); err != nil {
+ return fmt.Errorf("CreateUser: %v", err)
+ }
+
+ fmt.Printf("New user '%s' has been successfully created!\n", c.String("name"))
+ return nil
+}
+
+func adminDashboardOperation(operation func() error, successMessage string) func(*cli.Context) error {
+ return func(c *cli.Context) error {
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+
+ setting.NewContext()
+ db.LoadConfigs()
+ db.SetEngine()
+
+ if err := operation(); err != nil {
+ functionName := runtime.FuncForPC(reflect.ValueOf(operation).Pointer()).Name()
+ return fmt.Errorf("%s: %v", functionName, err)
+ }
+
+ fmt.Printf("%s\n", successMessage)
+ return nil
+ }
+}
diff --git a/internal/cmd/backup.go b/internal/cmd/backup.go
new file mode 100644
index 00000000..e918d486
--- /dev/null
+++ b/internal/cmd/backup.go
@@ -0,0 +1,137 @@
+// Copyright 2017 The Gogs 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 cmd
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path"
+ "time"
+
+ "github.com/unknwon/cae/zip"
+ "github.com/unknwon/com"
+ "github.com/urfave/cli"
+ log "gopkg.in/clog.v1"
+ "gopkg.in/ini.v1"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/setting"
+)
+
+var Backup = cli.Command{
+ Name: "backup",
+ Usage: "Backup files and database",
+ Description: `Backup dumps and compresses all related files and database into zip file,
+which can be used for migrating Gogs to another server. The output format is meant to be
+portable among all supported database engines.`,
+ Action: runBackup,
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ boolFlag("verbose, v", "Show process details"),
+ stringFlag("tempdir, t", os.TempDir(), "Temporary directory path"),
+ stringFlag("target", "./", "Target directory path to save backup archive"),
+ stringFlag("archive-name", fmt.Sprintf("gogs-backup-%s.zip", time.Now().Format("20060102150405")), "Name of backup archive"),
+ boolFlag("database-only", "Only dump database"),
+ boolFlag("exclude-repos", "Exclude repositories"),
+ },
+}
+
+const _CURRENT_BACKUP_FORMAT_VERSION = 1
+const _ARCHIVE_ROOT_DIR = "gogs-backup"
+
+func runBackup(c *cli.Context) error {
+ zip.Verbose = c.Bool("verbose")
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+ setting.NewContext()
+ db.LoadConfigs()
+ db.SetEngine()
+
+ tmpDir := c.String("tempdir")
+ if !com.IsExist(tmpDir) {
+ log.Fatal(0, "'--tempdir' does not exist: %s", tmpDir)
+ }
+ rootDir, err := ioutil.TempDir(tmpDir, "gogs-backup-")
+ if err != nil {
+ log.Fatal(0, "Fail to create backup root directory '%s': %v", rootDir, err)
+ }
+ log.Info("Backup root directory: %s", rootDir)
+
+ // Metadata
+ metaFile := path.Join(rootDir, "metadata.ini")
+ metadata := ini.Empty()
+ metadata.Section("").Key("VERSION").SetValue(com.ToStr(_CURRENT_BACKUP_FORMAT_VERSION))
+ metadata.Section("").Key("DATE_TIME").SetValue(time.Now().String())
+ metadata.Section("").Key("GOGS_VERSION").SetValue(setting.AppVer)
+ if err = metadata.SaveTo(metaFile); err != nil {
+ log.Fatal(0, "Fail to save metadata '%s': %v", metaFile, err)
+ }
+
+ archiveName := path.Join(c.String("target"), c.String("archive-name"))
+ log.Info("Packing backup files to: %s", archiveName)
+
+ z, err := zip.Create(archiveName)
+ if err != nil {
+ log.Fatal(0, "Fail to create backup archive '%s': %v", archiveName, err)
+ }
+ if err = z.AddFile(_ARCHIVE_ROOT_DIR+"/metadata.ini", metaFile); err != nil {
+ log.Fatal(0, "Fail to include 'metadata.ini': %v", err)
+ }
+
+ // Database
+ dbDir := path.Join(rootDir, "db")
+ if err = db.DumpDatabase(dbDir); err != nil {
+ log.Fatal(0, "Fail to dump database: %v", err)
+ }
+ if err = z.AddDir(_ARCHIVE_ROOT_DIR+"/db", dbDir); err != nil {
+ log.Fatal(0, "Fail to include 'db': %v", err)
+ }
+
+ // Custom files
+ if !c.Bool("database-only") {
+ if err = z.AddDir(_ARCHIVE_ROOT_DIR+"/custom", setting.CustomPath); err != nil {
+ log.Fatal(0, "Fail to include 'custom': %v", err)
+ }
+ }
+
+ // Data files
+ if !c.Bool("database-only") {
+ for _, dir := range []string{"attachments", "avatars", "repo-avatars"} {
+ dirPath := path.Join(setting.AppDataPath, dir)
+ if !com.IsDir(dirPath) {
+ continue
+ }
+
+ if err = z.AddDir(path.Join(_ARCHIVE_ROOT_DIR+"/data", dir), dirPath); err != nil {
+ log.Fatal(0, "Fail to include 'data': %v", err)
+ }
+ }
+ }
+
+ // Repositories
+ if !c.Bool("exclude-repos") && !c.Bool("database-only") {
+ reposDump := path.Join(rootDir, "repositories.zip")
+ log.Info("Dumping repositories in '%s'", setting.RepoRootPath)
+ if err = zip.PackTo(setting.RepoRootPath, reposDump, true); err != nil {
+ log.Fatal(0, "Fail to dump repositories: %v", err)
+ }
+ log.Info("Repositories dumped to: %s", reposDump)
+
+ if err = z.AddFile(_ARCHIVE_ROOT_DIR+"/repositories.zip", reposDump); err != nil {
+ log.Fatal(0, "Fail to include 'repositories.zip': %v", err)
+ }
+ }
+
+ if err = z.Close(); err != nil {
+ log.Fatal(0, "Fail to save backup archive '%s': %v", archiveName, err)
+ }
+
+ os.RemoveAll(rootDir)
+ log.Info("Backup succeed! Archive is located at: %s", archiveName)
+ log.Shutdown()
+ return nil
+}
diff --git a/internal/cmd/cert.go b/internal/cmd/cert.go
new file mode 100644
index 00000000..7c91c2c6
--- /dev/null
+++ b/internal/cmd/cert.go
@@ -0,0 +1,163 @@
+// +build cert
+
+// Copyright 2009 The Go Authors. All rights reserved.
+// Copyright 2014 The Gogs 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 cmd
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "log"
+ "math/big"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/urfave/cli"
+)
+
+var Cert = cli.Command{
+ Name: "cert",
+ Usage: "Generate self-signed certificate",
+ Description: `Generate a self-signed X.509 certificate for a TLS server.
+Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`,
+ Action: runCert,
+ Flags: []cli.Flag{
+ stringFlag("host", "", "Comma-separated hostnames and IPs to generate a certificate for"),
+ stringFlag("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521"),
+ intFlag("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set"),
+ stringFlag("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011"),
+ durationFlag("duration", 365*24*time.Hour, "Duration that certificate is valid for"),
+ boolFlag("ca", "whether this cert should be its own Certificate Authority"),
+ },
+}
+
+func publicKey(priv interface{}) interface{} {
+ switch k := priv.(type) {
+ case *rsa.PrivateKey:
+ return &k.PublicKey
+ case *ecdsa.PrivateKey:
+ return &k.PublicKey
+ default:
+ return nil
+ }
+}
+
+func pemBlockForKey(priv interface{}) *pem.Block {
+ switch k := priv.(type) {
+ case *rsa.PrivateKey:
+ return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
+ case *ecdsa.PrivateKey:
+ b, err := x509.MarshalECPrivateKey(k)
+ if err != nil {
+ log.Fatalf("Unable to marshal ECDSA private key: %v\n", err)
+ }
+ return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
+ default:
+ return nil
+ }
+}
+
+func runCert(ctx *cli.Context) error {
+ if len(ctx.String("host")) == 0 {
+ log.Fatal("Missing required --host parameter")
+ }
+
+ var priv interface{}
+ var err error
+ switch ctx.String("ecdsa-curve") {
+ case "":
+ priv, err = rsa.GenerateKey(rand.Reader, ctx.Int("rsa-bits"))
+ case "P224":
+ priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
+ case "P256":
+ priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ case "P384":
+ priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
+ case "P521":
+ priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+ default:
+ log.Fatalf("Unrecognized elliptic curve: %q", ctx.String("ecdsa-curve"))
+ }
+ if err != nil {
+ log.Fatalf("Failed to generate private key: %s", err)
+ }
+
+ var notBefore time.Time
+ if len(ctx.String("start-date")) == 0 {
+ notBefore = time.Now()
+ } else {
+ notBefore, err = time.Parse("Jan 2 15:04:05 2006", ctx.String("start-date"))
+ if err != nil {
+ log.Fatalf("Failed to parse creation date: %s", err)
+ }
+ }
+
+ notAfter := notBefore.Add(ctx.Duration("duration"))
+
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ log.Fatalf("Failed to generate serial number: %s", err)
+ }
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ Organization: []string{"Acme Co"},
+ CommonName: "Gogs",
+ },
+ NotBefore: notBefore,
+ NotAfter: notAfter,
+
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ }
+
+ hosts := strings.Split(ctx.String("host"), ",")
+ for _, h := range hosts {
+ if ip := net.ParseIP(h); ip != nil {
+ template.IPAddresses = append(template.IPAddresses, ip)
+ } else {
+ template.DNSNames = append(template.DNSNames, h)
+ }
+ }
+
+ if ctx.Bool("ca") {
+ template.IsCA = true
+ template.KeyUsage |= x509.KeyUsageCertSign
+ }
+
+ derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
+ if err != nil {
+ log.Fatalf("Failed to create certificate: %s", err)
+ }
+
+ certOut, err := os.Create("cert.pem")
+ if err != nil {
+ log.Fatalf("Failed to open cert.pem for writing: %s", err)
+ }
+ pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
+ certOut.Close()
+ log.Println("Written cert.pem")
+
+ keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ log.Fatalf("Failed to open key.pem for writing: %v\n", err)
+ }
+ pem.Encode(keyOut, pemBlockForKey(priv))
+ keyOut.Close()
+ log.Println("Written key.pem")
+
+ return nil
+}
diff --git a/internal/cmd/cert_stub.go b/internal/cmd/cert_stub.go
new file mode 100644
index 00000000..6164c83e
--- /dev/null
+++ b/internal/cmd/cert_stub.go
@@ -0,0 +1,28 @@
+// +build !cert
+
+// Copyright 2009 The Go Authors. All rights reserved.
+// Copyright 2014 The Gogs 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 cmd
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/urfave/cli"
+)
+
+var Cert = cli.Command{
+ Name: "cert",
+ Usage: "Generate self-signed certificate",
+ Description: `Please use build tags "cert" to rebuild Gogs in order to have this ability`,
+ Action: runCert,
+}
+
+func runCert(ctx *cli.Context) error {
+ fmt.Println("Command cert not available, please use build tags 'cert' to rebuild.")
+ os.Exit(1)
+
+ return nil
+}
diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go
new file mode 100644
index 00000000..29afa625
--- /dev/null
+++ b/internal/cmd/cmd.go
@@ -0,0 +1,42 @@
+// Copyright 2015 The Gogs 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 cmd
+
+import (
+ "time"
+
+ "github.com/urfave/cli"
+)
+
+func stringFlag(name, value, usage string) cli.StringFlag {
+ return cli.StringFlag{
+ Name: name,
+ Value: value,
+ Usage: usage,
+ }
+}
+
+func boolFlag(name, usage string) cli.BoolFlag {
+ return cli.BoolFlag{
+ Name: name,
+ Usage: usage,
+ }
+}
+
+func intFlag(name string, value int, usage string) cli.IntFlag {
+ return cli.IntFlag{
+ Name: name,
+ Value: value,
+ Usage: usage,
+ }
+}
+
+func durationFlag(name string, value time.Duration, usage string) cli.DurationFlag {
+ return cli.DurationFlag{
+ Name: name,
+ Value: value,
+ Usage: usage,
+ }
+}
diff --git a/internal/cmd/hook.go b/internal/cmd/hook.go
new file mode 100644
index 00000000..a6c79c3b
--- /dev/null
+++ b/internal/cmd/hook.go
@@ -0,0 +1,277 @@
+// Copyright 2014 The Gogs 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 cmd
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/tls"
+ "fmt"
+ "os"
+ "os/exec"
+ "path"
+ "path/filepath"
+ "strings"
+
+ "github.com/unknwon/com"
+ "github.com/urfave/cli"
+ log "gopkg.in/clog.v1"
+
+ "github.com/gogs/git-module"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/httplib"
+ "gogs.io/gogs/internal/mailer"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/template"
+)
+
+var (
+ Hook = cli.Command{
+ Name: "hook",
+ Usage: "Delegate commands to corresponding Git hooks",
+ Description: "All sub-commands should only be called by Git",
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ Subcommands: []cli.Command{
+ subcmdHookPreReceive,
+ subcmdHookUpadte,
+ subcmdHookPostReceive,
+ },
+ }
+
+ subcmdHookPreReceive = cli.Command{
+ Name: "pre-receive",
+ Usage: "Delegate pre-receive Git hook",
+ Description: "This command should only be called by Git",
+ Action: runHookPreReceive,
+ }
+ subcmdHookUpadte = cli.Command{
+ Name: "update",
+ Usage: "Delegate update Git hook",
+ Description: "This command should only be called by Git",
+ Action: runHookUpdate,
+ }
+ subcmdHookPostReceive = cli.Command{
+ Name: "post-receive",
+ Usage: "Delegate post-receive Git hook",
+ Description: "This command should only be called by Git",
+ Action: runHookPostReceive,
+ }
+)
+
+func runHookPreReceive(c *cli.Context) error {
+ if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
+ return nil
+ }
+ setup(c, "hooks/pre-receive.log", true)
+
+ isWiki := strings.Contains(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
+
+ buf := bytes.NewBuffer(nil)
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ buf.Write(scanner.Bytes())
+ buf.WriteByte('\n')
+
+ if isWiki {
+ continue
+ }
+
+ fields := bytes.Fields(scanner.Bytes())
+ if len(fields) != 3 {
+ continue
+ }
+ oldCommitID := string(fields[0])
+ newCommitID := string(fields[1])
+ branchName := strings.TrimPrefix(string(fields[2]), git.BRANCH_PREFIX)
+
+ // Branch protection
+ repoID := com.StrTo(os.Getenv(db.ENV_REPO_ID)).MustInt64()
+ protectBranch, err := db.GetProtectBranchOfRepoByName(repoID, branchName)
+ if err != nil {
+ if errors.IsErrBranchNotExist(err) {
+ continue
+ }
+ fail("Internal error", "GetProtectBranchOfRepoByName [repo_id: %d, branch: %s]: %v", repoID, branchName, err)
+ }
+ if !protectBranch.Protected {
+ continue
+ }
+
+ // Whitelist users can bypass require pull request check
+ bypassRequirePullRequest := false
+
+ // Check if user is in whitelist when enabled
+ userID := com.StrTo(os.Getenv(db.ENV_AUTH_USER_ID)).MustInt64()
+ if protectBranch.EnableWhitelist {
+ if !db.IsUserInProtectBranchWhitelist(repoID, userID, branchName) {
+ fail(fmt.Sprintf("Branch '%s' is protected and you are not in the push whitelist", branchName), "")
+ }
+
+ bypassRequirePullRequest = true
+ }
+
+ // Check if branch allows direct push
+ if !bypassRequirePullRequest && protectBranch.RequirePullRequest {
+ fail(fmt.Sprintf("Branch '%s' is protected and commits must be merged through pull request", branchName), "")
+ }
+
+ // check and deletion
+ if newCommitID == git.EMPTY_SHA {
+ fail(fmt.Sprintf("Branch '%s' is protected from deletion", branchName), "")
+ }
+
+ // Check force push
+ output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).
+ RunInDir(db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME)))
+ 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), "")
+ }
+ }
+
+ customHooksPath := filepath.Join(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), "pre-receive")
+ if !com.IsFile(customHooksPath) {
+ return nil
+ }
+
+ var hookCmd *exec.Cmd
+ if setting.IsWindows {
+ hookCmd = exec.Command("bash.exe", "custom_hooks/pre-receive")
+ } else {
+ hookCmd = exec.Command(customHooksPath)
+ }
+ hookCmd.Dir = db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME))
+ hookCmd.Stdout = os.Stdout
+ hookCmd.Stdin = buf
+ hookCmd.Stderr = os.Stderr
+ if err := hookCmd.Run(); err != nil {
+ fail("Internal error", "Fail to execute custom pre-receive hook: %v", err)
+ }
+ return nil
+}
+
+func runHookUpdate(c *cli.Context) error {
+ if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
+ return nil
+ }
+ setup(c, "hooks/update.log", false)
+
+ args := c.Args()
+ if len(args) != 3 {
+ fail("Arguments received are not equal to three", "Arguments received are not equal to three")
+ } else if len(args[0]) == 0 {
+ fail("First argument 'refName' is empty", "First argument 'refName' is empty")
+ }
+
+ customHooksPath := filepath.Join(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), "update")
+ if !com.IsFile(customHooksPath) {
+ return nil
+ }
+
+ var hookCmd *exec.Cmd
+ if setting.IsWindows {
+ hookCmd = exec.Command("bash.exe", append([]string{"custom_hooks/update"}, args...)...)
+ } else {
+ hookCmd = exec.Command(customHooksPath, args...)
+ }
+ hookCmd.Dir = db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME))
+ hookCmd.Stdout = os.Stdout
+ hookCmd.Stdin = os.Stdin
+ hookCmd.Stderr = os.Stderr
+ if err := hookCmd.Run(); err != nil {
+ fail("Internal error", "Fail to execute custom pre-receive hook: %v", err)
+ }
+ return nil
+}
+
+func runHookPostReceive(c *cli.Context) error {
+ if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
+ return nil
+ }
+ setup(c, "hooks/post-receive.log", true)
+
+ // Post-receive hook does more than just gather Git information,
+ // so we need to setup additional services for email notifications.
+ setting.NewPostReceiveHookServices()
+ mailer.NewContext()
+ mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"),
+ path.Join(setting.CustomPath, "templates/mail"), template.NewFuncMap())
+
+ isWiki := strings.Contains(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
+
+ buf := bytes.NewBuffer(nil)
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ buf.Write(scanner.Bytes())
+ buf.WriteByte('\n')
+
+ // TODO: support news feeds for wiki
+ if isWiki {
+ continue
+ }
+
+ fields := bytes.Fields(scanner.Bytes())
+ if len(fields) != 3 {
+ continue
+ }
+
+ options := db.PushUpdateOptions{
+ OldCommitID: string(fields[0]),
+ NewCommitID: string(fields[1]),
+ RefFullName: string(fields[2]),
+ PusherID: com.StrTo(os.Getenv(db.ENV_AUTH_USER_ID)).MustInt64(),
+ PusherName: os.Getenv(db.ENV_AUTH_USER_NAME),
+ RepoUserName: os.Getenv(db.ENV_REPO_OWNER_NAME),
+ RepoName: os.Getenv(db.ENV_REPO_NAME),
+ }
+ if err := db.PushUpdate(options); err != nil {
+ log.Error(2, "PushUpdate: %v", err)
+ }
+
+ // Ask for running deliver hook and test pull request tasks
+ reqURL := setting.LocalURL + options.RepoUserName + "/" + options.RepoName + "/tasks/trigger?branch=" +
+ template.EscapePound(strings.TrimPrefix(options.RefFullName, git.BRANCH_PREFIX)) +
+ "&secret=" + os.Getenv(db.ENV_REPO_OWNER_SALT_MD5) +
+ "&pusher=" + os.Getenv(db.ENV_AUTH_USER_ID)
+ log.Trace("Trigger task: %s", reqURL)
+
+ resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
+ InsecureSkipVerify: true,
+ }).Response()
+ if err == nil {
+ resp.Body.Close()
+ if resp.StatusCode/100 != 2 {
+ log.Error(2, "Fail to trigger task: not 2xx response code")
+ }
+ } else {
+ log.Error(2, "Fail to trigger task: %v", err)
+ }
+ }
+
+ customHooksPath := filepath.Join(os.Getenv(db.ENV_REPO_CUSTOM_HOOKS_PATH), "post-receive")
+ if !com.IsFile(customHooksPath) {
+ return nil
+ }
+
+ var hookCmd *exec.Cmd
+ if setting.IsWindows {
+ hookCmd = exec.Command("bash.exe", "custom_hooks/post-receive")
+ } else {
+ hookCmd = exec.Command(customHooksPath)
+ }
+ hookCmd.Dir = db.RepoPath(os.Getenv(db.ENV_REPO_OWNER_NAME), os.Getenv(db.ENV_REPO_NAME))
+ hookCmd.Stdout = os.Stdout
+ hookCmd.Stdin = buf
+ hookCmd.Stderr = os.Stderr
+ if err := hookCmd.Run(); err != nil {
+ fail("Internal error", "Fail to execute custom post-receive hook: %v", err)
+ }
+ return nil
+}
diff --git a/internal/cmd/import.go b/internal/cmd/import.go
new file mode 100644
index 00000000..4ee43b5b
--- /dev/null
+++ b/internal/cmd/import.go
@@ -0,0 +1,113 @@
+// Copyright 2016 The Gogs 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 cmd
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/unknwon/com"
+ "github.com/urfave/cli"
+
+ "gogs.io/gogs/internal/setting"
+)
+
+var (
+ Import = cli.Command{
+ Name: "import",
+ Usage: "Import portable data as local Gogs data",
+ Description: `Allow user import data from other Gogs installations to local instance
+without manually hacking the data files`,
+ Subcommands: []cli.Command{
+ subcmdImportLocale,
+ },
+ }
+
+ subcmdImportLocale = cli.Command{
+ Name: "locale",
+ Usage: "Import locale files to local repository",
+ Action: runImportLocale,
+ Flags: []cli.Flag{
+ stringFlag("source", "", "Source directory that stores new locale files"),
+ stringFlag("target", "", "Target directory that stores old locale files"),
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+ }
+)
+
+func runImportLocale(c *cli.Context) error {
+ if !c.IsSet("source") {
+ return fmt.Errorf("Source directory is not specified")
+ } else if !c.IsSet("target") {
+ return fmt.Errorf("Target directory is not specified")
+ }
+ if !com.IsDir(c.String("source")) {
+ return fmt.Errorf("Source directory does not exist or is not a directory")
+ } else if !com.IsDir(c.String("target")) {
+ return fmt.Errorf("Target directory does not exist or is not a directory")
+ }
+
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+
+ setting.NewContext()
+
+ now := time.Now()
+
+ line := make([]byte, 0, 100)
+ badChars := []byte(`="`)
+ escapedQuotes := []byte(`\"`)
+ regularQuotes := []byte(`"`)
+ // Cut out en-US.
+ for _, lang := range setting.Langs[1:] {
+ name := fmt.Sprintf("locale_%s.ini", lang)
+ source := filepath.Join(c.String("source"), name)
+ target := filepath.Join(c.String("target"), name)
+ if !com.IsFile(source) {
+ continue
+ }
+
+ // Crowdin surrounds double quotes for strings contain quotes inside,
+ // this breaks INI parser, we need to fix that.
+ sr, err := os.Open(source)
+ if err != nil {
+ return fmt.Errorf("Open: %v", err)
+ }
+
+ tw, err := os.Create(target)
+ if err != nil {
+ if err != nil {
+ return fmt.Errorf("Open: %v", err)
+ }
+ }
+
+ scanner := bufio.NewScanner(sr)
+ for scanner.Scan() {
+ line = scanner.Bytes()
+ idx := bytes.Index(line, badChars)
+ if idx > -1 && line[len(line)-1] == '"' {
+ // We still want the "=" sign
+ line = append(line[:idx+1], line[idx+2:len(line)-1]...)
+ line = bytes.Replace(line, escapedQuotes, regularQuotes, -1)
+ }
+ tw.Write(line)
+ tw.WriteString("\n")
+ }
+ sr.Close()
+ tw.Close()
+
+ // Modification time of files from Crowdin often ahead of current,
+ // so we need to set back to current.
+ os.Chtimes(target, now, now)
+ }
+
+ fmt.Println("Locale files has been successfully imported!")
+ return nil
+}
diff --git a/internal/cmd/restore.go b/internal/cmd/restore.go
new file mode 100644
index 00000000..cd5595df
--- /dev/null
+++ b/internal/cmd/restore.go
@@ -0,0 +1,148 @@
+// Copyright 2017 The Gogs 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 cmd
+
+import (
+ "os"
+ "path"
+
+ "github.com/mcuadros/go-version"
+ "github.com/unknwon/cae/zip"
+ "github.com/unknwon/com"
+ "github.com/urfave/cli"
+ log "gopkg.in/clog.v1"
+ "gopkg.in/ini.v1"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/setting"
+)
+
+var Restore = cli.Command{
+ Name: "restore",
+ Usage: "Restore files and database from backup",
+ Description: `Restore imports all related files and database from a backup archive.
+The backup version must lower or equal to current Gogs version. You can also import
+backup from other database engines, which is useful for database migrating.
+
+If corresponding files or database tables are not presented in the archive, they will
+be skipped and remain unchanged.`,
+ Action: runRestore,
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ boolFlag("verbose, v", "Show process details"),
+ stringFlag("tempdir, t", os.TempDir(), "Temporary directory path"),
+ stringFlag("from", "", "Path to backup archive"),
+ boolFlag("database-only", "Only import database"),
+ boolFlag("exclude-repos", "Exclude repositories"),
+ },
+}
+
+// lastSupportedVersionOfFormat returns the last supported version of the backup archive
+// format that is able to import.
+var lastSupportedVersionOfFormat = map[int]string{}
+
+func runRestore(c *cli.Context) error {
+ zip.Verbose = c.Bool("verbose")
+
+ tmpDir := c.String("tempdir")
+ if !com.IsExist(tmpDir) {
+ log.Fatal(0, "'--tempdir' does not exist: %s", tmpDir)
+ }
+
+ log.Info("Restore backup from: %s", c.String("from"))
+ if err := zip.ExtractTo(c.String("from"), tmpDir); err != nil {
+ log.Fatal(0, "Failed to extract backup archive: %v", err)
+ }
+ archivePath := path.Join(tmpDir, _ARCHIVE_ROOT_DIR)
+ defer os.RemoveAll(archivePath)
+
+ // Check backup version
+ metaFile := path.Join(archivePath, "metadata.ini")
+ if !com.IsExist(metaFile) {
+ log.Fatal(0, "File 'metadata.ini' is missing")
+ }
+ metadata, err := ini.Load(metaFile)
+ if err != nil {
+ log.Fatal(0, "Failed to load metadata '%s': %v", metaFile, err)
+ }
+ backupVersion := metadata.Section("").Key("GOGS_VERSION").MustString("999.0")
+ if version.Compare(setting.AppVer, backupVersion, "<") {
+ log.Fatal(0, "Current Gogs version is lower than backup version: %s < %s", setting.AppVer, backupVersion)
+ }
+ formatVersion := metadata.Section("").Key("VERSION").MustInt()
+ if formatVersion == 0 {
+ log.Fatal(0, "Failed to determine the backup format version from metadata '%s': %s", metaFile, "VERSION is not presented")
+ }
+ if formatVersion != _CURRENT_BACKUP_FORMAT_VERSION {
+ log.Fatal(0, "Backup format version found is %d but this binary only supports %d\nThe last known version that is able to import your backup is %s",
+ formatVersion, _CURRENT_BACKUP_FORMAT_VERSION, lastSupportedVersionOfFormat[formatVersion])
+ }
+
+ // If config file is not present in backup, user must set this file via flag.
+ // Otherwise, it's optional to set config file flag.
+ configFile := path.Join(archivePath, "custom/conf/app.ini")
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ } else if !com.IsExist(configFile) {
+ log.Fatal(0, "'--config' is not specified and custom config file is not found in backup")
+ } else {
+ setting.CustomConf = configFile
+ }
+ setting.NewContext()
+ db.LoadConfigs()
+ db.SetEngine()
+
+ // Database
+ dbDir := path.Join(archivePath, "db")
+ if err = db.ImportDatabase(dbDir, c.Bool("verbose")); err != nil {
+ log.Fatal(0, "Failed to import database: %v", err)
+ }
+
+ // Custom files
+ if !c.Bool("database-only") {
+ if com.IsExist(setting.CustomPath) {
+ if err = os.Rename(setting.CustomPath, setting.CustomPath+".bak"); err != nil {
+ log.Fatal(0, "Failed to backup current 'custom': %v", err)
+ }
+ }
+ if err = os.Rename(path.Join(archivePath, "custom"), setting.CustomPath); err != nil {
+ log.Fatal(0, "Failed to import 'custom': %v", err)
+ }
+ }
+
+ // Data files
+ if !c.Bool("database-only") {
+ os.MkdirAll(setting.AppDataPath, os.ModePerm)
+ for _, dir := range []string{"attachments", "avatars", "repo-avatars"} {
+ // Skip if backup archive does not have corresponding data
+ srcPath := path.Join(archivePath, "data", dir)
+ if !com.IsDir(srcPath) {
+ continue
+ }
+
+ dirPath := path.Join(setting.AppDataPath, dir)
+ if com.IsExist(dirPath) {
+ if err = os.Rename(dirPath, dirPath+".bak"); err != nil {
+ log.Fatal(0, "Failed to backup current 'data': %v", err)
+ }
+ }
+ if err = os.Rename(srcPath, dirPath); err != nil {
+ log.Fatal(0, "Failed to import 'data': %v", err)
+ }
+ }
+ }
+
+ // Repositories
+ reposPath := path.Join(archivePath, "repositories.zip")
+ if !c.Bool("exclude-repos") && !c.Bool("database-only") && com.IsExist(reposPath) {
+ if err := zip.ExtractTo(reposPath, path.Dir(setting.RepoRootPath)); err != nil {
+ log.Fatal(0, "Failed to extract 'repositories.zip': %v", err)
+ }
+ }
+
+ log.Info("Restore succeed!")
+ log.Shutdown()
+ return nil
+}
diff --git a/internal/cmd/serv.go b/internal/cmd/serv.go
new file mode 100644
index 00000000..870cf62e
--- /dev/null
+++ b/internal/cmd/serv.go
@@ -0,0 +1,272 @@
+// Copyright 2014 The Gogs 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 cmd
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/unknwon/com"
+ "github.com/urfave/cli"
+ log "gopkg.in/clog.v1"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+)
+
+const (
+ _ACCESS_DENIED_MESSAGE = "Repository does not exist or you do not have access"
+)
+
+var Serv = cli.Command{
+ Name: "serv",
+ Usage: "This command should only be called by SSH shell",
+ Description: `Serv provide access auth for repositories`,
+ Action: runServ,
+ Flags: []cli.Flag{
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+}
+
+func fail(userMessage, logMessage string, args ...interface{}) {
+ fmt.Fprintln(os.Stderr, "Gogs:", userMessage)
+
+ if len(logMessage) > 0 {
+ if !setting.ProdMode {
+ fmt.Fprintf(os.Stderr, logMessage+"\n", args...)
+ }
+ log.Fatal(3, logMessage, args...)
+ }
+
+ os.Exit(1)
+}
+
+func setup(c *cli.Context, logPath string, connectDB bool) {
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ } else if c.GlobalIsSet("config") {
+ setting.CustomConf = c.GlobalString("config")
+ }
+
+ setting.NewContext()
+
+ level := log.TRACE
+ if setting.ProdMode {
+ level = log.ERROR
+ }
+ log.New(log.FILE, log.FileConfig{
+ Level: level,
+ Filename: filepath.Join(setting.LogRootPath, logPath),
+ FileRotationConfig: log.FileRotationConfig{
+ Rotate: true,
+ Daily: true,
+ MaxDays: 3,
+ },
+ })
+ log.Delete(log.CONSOLE) // Remove primary logger
+
+ if !connectDB {
+ return
+ }
+
+ db.LoadConfigs()
+
+ if setting.UseSQLite3 {
+ workDir, _ := setting.WorkDir()
+ os.Chdir(workDir)
+ }
+
+ if err := db.SetEngine(); err != nil {
+ fail("Internal error", "SetEngine: %v", err)
+ }
+}
+
+func parseSSHCmd(cmd string) (string, string) {
+ ss := strings.SplitN(cmd, " ", 2)
+ if len(ss) != 2 {
+ return "", ""
+ }
+ return ss[0], strings.Replace(ss[1], "'/", "'", 1)
+}
+
+func checkDeployKey(key *db.PublicKey, repo *db.Repository) {
+ // Check if this deploy key belongs to current repository.
+ if !db.HasDeployKey(key.ID, repo.ID) {
+ fail("Key access denied", "Deploy key access denied: [key_id: %d, repo_id: %d]", key.ID, repo.ID)
+ }
+
+ // Update deploy key activity.
+ deployKey, err := db.GetDeployKeyByRepo(key.ID, repo.ID)
+ if err != nil {
+ fail("Internal error", "GetDeployKey: %v", err)
+ }
+
+ deployKey.Updated = time.Now()
+ if err = db.UpdateDeployKey(deployKey); err != nil {
+ fail("Internal error", "UpdateDeployKey: %v", err)
+ }
+}
+
+var (
+ allowedCommands = map[string]db.AccessMode{
+ "git-upload-pack": db.ACCESS_MODE_READ,
+ "git-upload-archive": db.ACCESS_MODE_READ,
+ "git-receive-pack": db.ACCESS_MODE_WRITE,
+ }
+)
+
+func runServ(c *cli.Context) error {
+ setup(c, "serv.log", true)
+
+ if setting.SSH.Disabled {
+ println("Gogs: SSH has been disabled")
+ return nil
+ }
+
+ if len(c.Args()) < 1 {
+ fail("Not enough arguments", "Not enough arguments")
+ }
+
+ sshCmd := os.Getenv("SSH_ORIGINAL_COMMAND")
+ if len(sshCmd) == 0 {
+ println("Hi there, You've successfully authenticated, but Gogs does not provide shell access.")
+ println("If this is unexpected, please log in with password and setup Gogs under another user.")
+ return nil
+ }
+
+ verb, args := parseSSHCmd(sshCmd)
+ repoFullName := strings.ToLower(strings.Trim(args, "'"))
+ repoFields := strings.SplitN(repoFullName, "/", 2)
+ if len(repoFields) != 2 {
+ fail("Invalid repository path", "Invalid repository path: %v", args)
+ }
+ ownerName := strings.ToLower(repoFields[0])
+ repoName := strings.TrimSuffix(strings.ToLower(repoFields[1]), ".git")
+ repoName = strings.TrimSuffix(repoName, ".wiki")
+
+ owner, err := db.GetUserByName(ownerName)
+ if err != nil {
+ if errors.IsUserNotExist(err) {
+ fail("Repository owner does not exist", "Unregistered owner: %s", ownerName)
+ }
+ fail("Internal error", "Fail to get repository owner '%s': %v", ownerName, err)
+ }
+
+ repo, err := db.GetRepositoryByName(owner.ID, repoName)
+ if err != nil {
+ if errors.IsRepoNotExist(err) {
+ fail(_ACCESS_DENIED_MESSAGE, "Repository does not exist: %s/%s", owner.Name, repoName)
+ }
+ fail("Internal error", "Fail to get repository: %v", err)
+ }
+ repo.Owner = owner
+
+ requestMode, ok := allowedCommands[verb]
+ if !ok {
+ fail("Unknown git command", "Unknown git command '%s'", verb)
+ }
+
+ // Prohibit push to mirror repositories.
+ if requestMode > db.ACCESS_MODE_READ && repo.IsMirror {
+ fail("Mirror repository is read-only", "")
+ }
+
+ // Allow anonymous (user is nil) clone for public repositories.
+ var user *db.User
+
+ key, err := db.GetPublicKeyByID(com.StrTo(strings.TrimPrefix(c.Args()[0], "key-")).MustInt64())
+ if err != nil {
+ fail("Invalid key ID", "Invalid key ID '%s': %v", c.Args()[0], err)
+ }
+
+ if requestMode == db.ACCESS_MODE_WRITE || repo.IsPrivate {
+ // Check deploy key or user key.
+ if key.IsDeployKey() {
+ if key.Mode < requestMode {
+ fail("Key permission denied", "Cannot push with deployment key: %d", key.ID)
+ }
+ checkDeployKey(key, repo)
+ } else {
+ user, err = db.GetUserByKeyID(key.ID)
+ if err != nil {
+ fail("Internal error", "Fail to get user by key ID '%d': %v", key.ID, err)
+ }
+
+ mode, err := db.UserAccessMode(user.ID, repo)
+ if err != nil {
+ fail("Internal error", "Fail to check access: %v", err)
+ }
+
+ if mode < requestMode {
+ clientMessage := _ACCESS_DENIED_MESSAGE
+ if mode >= db.ACCESS_MODE_READ {
+ clientMessage = "You do not have sufficient authorization for this action"
+ }
+ fail(clientMessage,
+ "User '%s' does not have level '%v' access to repository '%s'",
+ user.Name, requestMode, repoFullName)
+ }
+ }
+ } else {
+ setting.NewService()
+ // Check if the key can access to the repository in case of it is a deploy key (a deploy keys != user key).
+ // A deploy key doesn't represent a signed in user, so in a site with Service.RequireSignInView activated
+ // we should give read access only in repositories where this deploy key is in use. In other case, a server
+ // or system using an active deploy key can get read access to all the repositories in a Gogs service.
+ if key.IsDeployKey() && setting.Service.RequireSignInView {
+ checkDeployKey(key, repo)
+ }
+ }
+
+ // Update user key activity.
+ if key.ID > 0 {
+ key, err := db.GetPublicKeyByID(key.ID)
+ if err != nil {
+ fail("Internal error", "GetPublicKeyByID: %v", err)
+ }
+
+ key.Updated = time.Now()
+ if err = db.UpdatePublicKey(key); err != nil {
+ fail("Internal error", "UpdatePublicKey: %v", err)
+ }
+ }
+
+ // Special handle for Windows.
+ if setting.IsWindows {
+ verb = strings.Replace(verb, "-", " ", 1)
+ }
+
+ var gitCmd *exec.Cmd
+ verbs := strings.Split(verb, " ")
+ if len(verbs) == 2 {
+ gitCmd = exec.Command(verbs[0], verbs[1], repoFullName)
+ } else {
+ gitCmd = exec.Command(verb, repoFullName)
+ }
+ if requestMode == db.ACCESS_MODE_WRITE {
+ gitCmd.Env = append(os.Environ(), db.ComposeHookEnvs(db.ComposeHookEnvsOptions{
+ AuthUser: user,
+ OwnerName: owner.Name,
+ OwnerSalt: owner.Salt,
+ RepoID: repo.ID,
+ RepoName: repo.Name,
+ RepoPath: repo.RepoPath(),
+ })...)
+ }
+ gitCmd.Dir = setting.RepoRootPath
+ gitCmd.Stdout = os.Stdout
+ gitCmd.Stdin = os.Stdin
+ gitCmd.Stderr = os.Stderr
+ if err = gitCmd.Run(); err != nil {
+ fail("Internal error", "Fail to execute git command: %v", err)
+ }
+
+ return nil
+}
diff --git a/internal/cmd/web.go b/internal/cmd/web.go
new file mode 100644
index 00000000..709bc0df
--- /dev/null
+++ b/internal/cmd/web.go
@@ -0,0 +1,757 @@
+// Copyright 2014 The Gogs 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 cmd
+
+import (
+ "crypto/tls"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/http"
+ "net/http/fcgi"
+ "os"
+ "path"
+ "strings"
+
+ "github.com/go-macaron/binding"
+ "github.com/go-macaron/cache"
+ "github.com/go-macaron/captcha"
+ "github.com/go-macaron/csrf"
+ "github.com/go-macaron/gzip"
+ "github.com/go-macaron/i18n"
+ "github.com/go-macaron/session"
+ "github.com/go-macaron/toolbox"
+ "github.com/mcuadros/go-version"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/unknwon/com"
+ "github.com/urfave/cli"
+ log "gopkg.in/clog.v1"
+ "gopkg.in/macaron.v1"
+
+ "gogs.io/gogs/internal/bindata"
+ "gogs.io/gogs/internal/context"
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/form"
+ "gogs.io/gogs/internal/mailer"
+ "gogs.io/gogs/internal/route"
+ "gogs.io/gogs/internal/route/admin"
+ apiv1 "gogs.io/gogs/internal/route/api/v1"
+ "gogs.io/gogs/internal/route/dev"
+ "gogs.io/gogs/internal/route/org"
+ "gogs.io/gogs/internal/route/repo"
+ "gogs.io/gogs/internal/route/user"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/template"
+)
+
+var Web = cli.Command{
+ Name: "web",
+ Usage: "Start web server",
+ Description: `Gogs web server is the only thing you need to run,
+and it takes care of all the other things for you`,
+ Action: runWeb,
+ Flags: []cli.Flag{
+ stringFlag("port, p", "3000", "Temporary port number to prevent conflict"),
+ stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
+ },
+}
+
+// checkVersion checks if binary matches the version of templates files.
+func checkVersion() {
+ // Templates.
+ data, err := ioutil.ReadFile(setting.StaticRootPath + "/templates/.VERSION")
+ if err != nil {
+ log.Fatal(2, "Fail to read 'templates/.VERSION': %v", err)
+ }
+ tplVer := strings.TrimSpace(string(data))
+ if tplVer != setting.AppVer {
+ if version.Compare(tplVer, setting.AppVer, ">") {
+ log.Fatal(2, "Binary version is lower than template file version, did you forget to recompile Gogs?")
+ } else {
+ log.Fatal(2, "Binary version is higher than template file version, did you forget to update template files?")
+ }
+ }
+}
+
+// newMacaron initializes Macaron instance.
+func newMacaron() *macaron.Macaron {
+ m := macaron.New()
+ if !setting.DisableRouterLog {
+ m.Use(macaron.Logger())
+ }
+ m.Use(macaron.Recovery())
+ if setting.EnableGzip {
+ m.Use(gzip.Gziper())
+ }
+ if setting.Protocol == setting.SCHEME_FCGI {
+ m.SetURLPrefix(setting.AppSubURL)
+ }
+ m.Use(macaron.Static(
+ path.Join(setting.StaticRootPath, "public"),
+ macaron.StaticOptions{
+ SkipLogging: setting.DisableRouterLog,
+ },
+ ))
+ m.Use(macaron.Static(
+ setting.AvatarUploadPath,
+ macaron.StaticOptions{
+ Prefix: db.USER_AVATAR_URL_PREFIX,
+ SkipLogging: setting.DisableRouterLog,
+ },
+ ))
+ m.Use(macaron.Static(
+ setting.RepositoryAvatarUploadPath,
+ macaron.StaticOptions{
+ Prefix: db.REPO_AVATAR_URL_PREFIX,
+ SkipLogging: setting.DisableRouterLog,
+ },
+ ))
+
+ funcMap := template.NewFuncMap()
+ m.Use(macaron.Renderer(macaron.RenderOptions{
+ Directory: path.Join(setting.StaticRootPath, "templates"),
+ AppendDirectories: []string{path.Join(setting.CustomPath, "templates")},
+ Funcs: funcMap,
+ IndentJSON: macaron.Env != macaron.PROD,
+ }))
+ mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"),
+ path.Join(setting.CustomPath, "templates/mail"), funcMap)
+
+ localeNames, err := bindata.AssetDir("conf/locale")
+ if err != nil {
+ log.Fatal(4, "Fail to list locale files: %v", err)
+ }
+ localFiles := make(map[string][]byte)
+ for _, name := range localeNames {
+ localFiles[name] = bindata.MustAsset("conf/locale/" + name)
+ }
+ m.Use(i18n.I18n(i18n.Options{
+ SubURL: setting.AppSubURL,
+ Files: localFiles,
+ CustomDirectory: path.Join(setting.CustomPath, "conf/locale"),
+ Langs: setting.Langs,
+ Names: setting.Names,
+ DefaultLang: "en-US",
+ Redirect: true,
+ }))
+ m.Use(cache.Cacher(cache.Options{
+ Adapter: setting.CacheAdapter,
+ AdapterConfig: setting.CacheConn,
+ Interval: setting.CacheInterval,
+ }))
+ m.Use(captcha.Captchaer(captcha.Options{
+ SubURL: setting.AppSubURL,
+ }))
+ m.Use(session.Sessioner(setting.SessionConfig))
+ m.Use(csrf.Csrfer(csrf.Options{
+ Secret: setting.SecretKey,
+ Cookie: setting.CSRFCookieName,
+ SetCookie: true,
+ Header: "X-Csrf-Token",
+ CookiePath: setting.AppSubURL,
+ }))
+ m.Use(toolbox.Toolboxer(m, toolbox.Options{
+ HealthCheckFuncs: []*toolbox.HealthCheckFuncDesc{
+ &toolbox.HealthCheckFuncDesc{
+ Desc: "Database connection",
+ Func: db.Ping,
+ },
+ },
+ }))
+ m.Use(context.Contexter())
+ return m
+}
+
+func runWeb(c *cli.Context) error {
+ if c.IsSet("config") {
+ setting.CustomConf = c.String("config")
+ }
+ route.GlobalInit()
+ checkVersion()
+
+ m := newMacaron()
+
+ reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true})
+ ignSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: setting.Service.RequireSignInView})
+ ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
+ reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
+
+ bindIgnErr := binding.BindIgnErr
+
+ m.SetAutoHead(true)
+
+ // FIXME: not all route need go through same middlewares.
+ // Especially some AJAX requests, we can reduce middleware number to improve performance.
+ // Routers.
+ m.Get("/", ignSignIn, route.Home)
+ m.Group("/explore", func() {
+ m.Get("", func(c *context.Context) {
+ c.Redirect(setting.AppSubURL + "/explore/repos")
+ })
+ m.Get("/repos", route.ExploreRepos)
+ m.Get("/users", route.ExploreUsers)
+ m.Get("/organizations", route.ExploreOrganizations)
+ }, ignSignIn)
+ m.Combo("/install", route.InstallInit).Get(route.Install).
+ Post(bindIgnErr(form.Install{}), route.InstallPost)
+ m.Get("/^:type(issues|pulls)$", reqSignIn, user.Issues)
+
+ // ***** START: User *****
+ m.Group("/user", func() {
+ m.Group("/login", func() {
+ m.Combo("").Get(user.Login).
+ Post(bindIgnErr(form.SignIn{}), user.LoginPost)
+ m.Combo("/two_factor").Get(user.LoginTwoFactor).Post(user.LoginTwoFactorPost)
+ m.Combo("/two_factor_recovery_code").Get(user.LoginTwoFactorRecoveryCode).Post(user.LoginTwoFactorRecoveryCodePost)
+ })
+
+ m.Get("/sign_up", user.SignUp)
+ m.Post("/sign_up", bindIgnErr(form.Register{}), user.SignUpPost)
+ m.Get("/reset_password", user.ResetPasswd)
+ m.Post("/reset_password", user.ResetPasswdPost)
+ }, reqSignOut)
+
+ m.Group("/user/settings", func() {
+ m.Get("", user.Settings)
+ m.Post("", bindIgnErr(form.UpdateProfile{}), user.SettingsPost)
+ m.Combo("/avatar").Get(user.SettingsAvatar).
+ Post(binding.MultipartForm(form.Avatar{}), user.SettingsAvatarPost)
+ m.Post("/avatar/delete", user.SettingsDeleteAvatar)
+ m.Combo("/email").Get(user.SettingsEmails).
+ Post(bindIgnErr(form.AddEmail{}), user.SettingsEmailPost)
+ m.Post("/email/delete", user.DeleteEmail)
+ m.Get("/password", user.SettingsPassword)
+ m.Post("/password", bindIgnErr(form.ChangePassword{}), user.SettingsPasswordPost)
+ m.Combo("/ssh").Get(user.SettingsSSHKeys).
+ Post(bindIgnErr(form.AddSSHKey{}), user.SettingsSSHKeysPost)
+ m.Post("/ssh/delete", user.DeleteSSHKey)
+ m.Group("/security", func() {
+ m.Get("", user.SettingsSecurity)
+ m.Combo("/two_factor_enable").Get(user.SettingsTwoFactorEnable).
+ Post(user.SettingsTwoFactorEnablePost)
+ m.Combo("/two_factor_recovery_codes").Get(user.SettingsTwoFactorRecoveryCodes).
+ Post(user.SettingsTwoFactorRecoveryCodesPost)
+ m.Post("/two_factor_disable", user.SettingsTwoFactorDisable)
+ })
+ m.Group("/repositories", func() {
+ m.Get("", user.SettingsRepos)
+ m.Post("/leave", user.SettingsLeaveRepo)
+ })
+ m.Group("/organizations", func() {
+ m.Get("", user.SettingsOrganizations)
+ m.Post("/leave", user.SettingsLeaveOrganization)
+ })
+ m.Combo("/applications").Get(user.SettingsApplications).
+ Post(bindIgnErr(form.NewAccessToken{}), user.SettingsApplicationsPost)
+ m.Post("/applications/delete", user.SettingsDeleteApplication)
+ m.Route("/delete", "GET,POST", user.SettingsDelete)
+ }, reqSignIn, func(c *context.Context) {
+ c.Data["PageIsUserSettings"] = true
+ })
+
+ m.Group("/user", func() {
+ m.Any("/activate", user.Activate)
+ m.Any("/activate_email", user.ActivateEmail)
+ m.Get("/email2user", user.Email2User)
+ m.Get("/forget_password", user.ForgotPasswd)
+ m.Post("/forget_password", user.ForgotPasswdPost)
+ m.Post("/logout", user.SignOut)
+ })
+ // ***** END: User *****
+
+ reqAdmin := context.Toggle(&context.ToggleOptions{SignInRequired: true, AdminRequired: true})
+
+ // ***** START: Admin *****
+ m.Group("/admin", func() {
+ m.Get("", admin.Dashboard)
+ m.Get("/config", admin.Config)
+ m.Post("/config/test_mail", admin.SendTestMail)
+ m.Get("/monitor", admin.Monitor)
+
+ m.Group("/users", func() {
+ m.Get("", admin.Users)
+ m.Combo("/new").Get(admin.NewUser).Post(bindIgnErr(form.AdminCrateUser{}), admin.NewUserPost)
+ m.Combo("/:userid").Get(admin.EditUser).Post(bindIgnErr(form.AdminEditUser{}), admin.EditUserPost)
+ m.Post("/:userid/delete", admin.DeleteUser)
+ })
+
+ m.Group("/orgs", func() {
+ m.Get("", admin.Organizations)
+ })
+
+ m.Group("/repos", func() {
+ m.Get("", admin.Repos)
+ m.Post("/delete", admin.DeleteRepo)
+ })
+
+ m.Group("/auths", func() {
+ m.Get("", admin.Authentications)
+ m.Combo("/new").Get(admin.NewAuthSource).Post(bindIgnErr(form.Authentication{}), admin.NewAuthSourcePost)
+ m.Combo("/:authid").Get(admin.EditAuthSource).
+ Post(bindIgnErr(form.Authentication{}), admin.EditAuthSourcePost)
+ m.Post("/:authid/delete", admin.DeleteAuthSource)
+ })
+
+ m.Group("/notices", func() {
+ m.Get("", admin.Notices)
+ m.Post("/delete", admin.DeleteNotices)
+ m.Get("/empty", admin.EmptyNotices)
+ })
+ }, reqAdmin)
+ // ***** END: Admin *****
+
+ m.Group("", func() {
+ m.Group("/:username", func() {
+ m.Get("", user.Profile)
+ m.Get("/followers", user.Followers)
+ m.Get("/following", user.Following)
+ m.Get("/stars", user.Stars)
+ }, context.InjectParamsUser())
+
+ m.Get("/attachments/:uuid", func(c *context.Context) {
+ attach, err := db.GetAttachmentByUUID(c.Params(":uuid"))
+ if err != nil {
+ c.NotFoundOrServerError("GetAttachmentByUUID", db.IsErrAttachmentNotExist, err)
+ return
+ } else if !com.IsFile(attach.LocalPath()) {
+ c.NotFound()
+ return
+ }
+
+ fr, err := os.Open(attach.LocalPath())
+ if err != nil {
+ c.Handle(500, "Open", err)
+ return
+ }
+ defer fr.Close()
+
+ c.Header().Set("Cache-Control", "public,max-age=86400")
+ fmt.Println("attach.Name:", attach.Name)
+ c.Header().Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, attach.Name))
+ if err = repo.ServeData(c, attach.Name, fr); err != nil {
+ c.Handle(500, "ServeData", err)
+ return
+ }
+ })
+ m.Post("/issues/attachments", repo.UploadIssueAttachment)
+ m.Post("/releases/attachments", repo.UploadReleaseAttachment)
+ }, ignSignIn)
+
+ m.Group("/:username", func() {
+ m.Post("/action/:action", user.Action)
+ }, reqSignIn, context.InjectParamsUser())
+
+ if macaron.Env == macaron.DEV {
+ m.Get("/template/*", dev.TemplatePreview)
+ }
+
+ reqRepoAdmin := context.RequireRepoAdmin()
+ reqRepoWriter := context.RequireRepoWriter()
+
+ // ***** START: Organization *****
+ m.Group("/org", func() {
+ m.Group("", func() {
+ m.Get("/create", org.Create)
+ m.Post("/create", bindIgnErr(form.CreateOrg{}), org.CreatePost)
+ }, func(c *context.Context) {
+ if !c.User.CanCreateOrganization() {
+ c.NotFound()
+ }
+ })
+
+ m.Group("/:org", func() {
+ m.Get("/dashboard", user.Dashboard)
+ m.Get("/^:type(issues|pulls)$", user.Issues)
+ m.Get("/members", org.Members)
+ m.Get("/members/action/:action", org.MembersAction)
+
+ m.Get("/teams", org.Teams)
+ }, context.OrgAssignment(true))
+
+ m.Group("/:org", func() {
+ m.Get("/teams/:team", org.TeamMembers)
+ m.Get("/teams/:team/repositories", org.TeamRepositories)
+ m.Route("/teams/:team/action/:action", "GET,POST", org.TeamsAction)
+ m.Route("/teams/:team/action/repo/:action", "GET,POST", org.TeamsRepoAction)
+ }, context.OrgAssignment(true, false, true))
+
+ m.Group("/:org", func() {
+ m.Get("/teams/new", org.NewTeam)
+ m.Post("/teams/new", bindIgnErr(form.CreateTeam{}), org.NewTeamPost)
+ m.Get("/teams/:team/edit", org.EditTeam)
+ m.Post("/teams/:team/edit", bindIgnErr(form.CreateTeam{}), org.EditTeamPost)
+ m.Post("/teams/:team/delete", org.DeleteTeam)
+
+ m.Group("/settings", func() {
+ m.Combo("").Get(org.Settings).
+ Post(bindIgnErr(form.UpdateOrgSetting{}), org.SettingsPost)
+ m.Post("/avatar", binding.MultipartForm(form.Avatar{}), org.SettingsAvatar)
+ m.Post("/avatar/delete", org.SettingsDeleteAvatar)
+
+ m.Group("/hooks", func() {
+ m.Get("", org.Webhooks)
+ m.Post("/delete", org.DeleteWebhook)
+ m.Get("/:type/new", repo.WebhooksNew)
+ m.Post("/gogs/new", bindIgnErr(form.NewWebhook{}), repo.WebHooksNewPost)
+ m.Post("/slack/new", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksNewPost)
+ m.Get("/:id", repo.WebHooksEdit)
+ m.Post("/gogs/:id", bindIgnErr(form.NewWebhook{}), repo.WebHooksEditPost)
+ m.Post("/slack/:id", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksEditPost)
+ m.Post("/discord/:id", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/:id", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksEditPost)
+ })
+
+ m.Route("/delete", "GET,POST", org.SettingsDelete)
+ })
+
+ m.Route("/invitations/new", "GET,POST", org.Invitation)
+ }, context.OrgAssignment(true, true))
+ }, reqSignIn)
+ // ***** END: Organization *****
+
+ // ***** START: Repository *****
+ m.Group("/repo", func() {
+ m.Get("/create", repo.Create)
+ m.Post("/create", bindIgnErr(form.CreateRepo{}), repo.CreatePost)
+ m.Get("/migrate", repo.Migrate)
+ m.Post("/migrate", bindIgnErr(form.MigrateRepo{}), repo.MigratePost)
+ m.Combo("/fork/:repoid").Get(repo.Fork).
+ Post(bindIgnErr(form.CreateRepo{}), repo.ForkPost)
+ }, reqSignIn)
+
+ m.Group("/:username/:reponame", func() {
+ m.Group("/settings", func() {
+ m.Combo("").Get(repo.Settings).
+ Post(bindIgnErr(form.RepoSetting{}), repo.SettingsPost)
+ m.Combo("/avatar").Get(repo.SettingsAvatar).
+ Post(binding.MultipartForm(form.Avatar{}), repo.SettingsAvatarPost)
+ m.Post("/avatar/delete", repo.SettingsDeleteAvatar)
+ m.Group("/collaboration", func() {
+ m.Combo("").Get(repo.SettingsCollaboration).Post(repo.SettingsCollaborationPost)
+ m.Post("/access_mode", repo.ChangeCollaborationAccessMode)
+ m.Post("/delete", repo.DeleteCollaboration)
+ })
+ m.Group("/branches", func() {
+ m.Get("", repo.SettingsBranches)
+ m.Post("/default_branch", repo.UpdateDefaultBranch)
+ m.Combo("/*").Get(repo.SettingsProtectedBranch).
+ Post(bindIgnErr(form.ProtectBranch{}), repo.SettingsProtectedBranchPost)
+ }, func(c *context.Context) {
+ if c.Repo.Repository.IsMirror {
+ c.NotFound()
+ return
+ }
+ })
+
+ m.Group("/hooks", func() {
+ m.Get("", repo.Webhooks)
+ m.Post("/delete", repo.DeleteWebhook)
+ m.Get("/:type/new", repo.WebhooksNew)
+ m.Post("/gogs/new", bindIgnErr(form.NewWebhook{}), repo.WebHooksNewPost)
+ m.Post("/slack/new", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksNewPost)
+ m.Post("/discord/new", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksNewPost)
+ m.Post("/dingtalk/new", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksNewPost)
+ m.Post("/gogs/:id", bindIgnErr(form.NewWebhook{}), repo.WebHooksEditPost)
+ m.Post("/slack/:id", bindIgnErr(form.NewSlackHook{}), repo.SlackHooksEditPost)
+ m.Post("/discord/:id", bindIgnErr(form.NewDiscordHook{}), repo.DiscordHooksEditPost)
+ m.Post("/dingtalk/:id", bindIgnErr(form.NewDingtalkHook{}), repo.DingtalkHooksEditPost)
+
+ m.Group("/:id", func() {
+ m.Get("", repo.WebHooksEdit)
+ m.Post("/test", repo.TestWebhook)
+ m.Post("/redelivery", repo.RedeliveryWebhook)
+ })
+
+ m.Group("/git", func() {
+ m.Get("", repo.SettingsGitHooks)
+ m.Combo("/:name").Get(repo.SettingsGitHooksEdit).
+ Post(repo.SettingsGitHooksEditPost)
+ }, context.GitHookService())
+ })
+
+ m.Group("/keys", func() {
+ m.Combo("").Get(repo.SettingsDeployKeys).
+ Post(bindIgnErr(form.AddSSHKey{}), repo.SettingsDeployKeysPost)
+ m.Post("/delete", repo.DeleteDeployKey)
+ })
+
+ }, func(c *context.Context) {
+ c.Data["PageIsSettings"] = true
+ })
+ }, reqSignIn, context.RepoAssignment(), reqRepoAdmin, context.RepoRef())
+
+ m.Post("/:username/:reponame/action/:action", reqSignIn, context.RepoAssignment(), repo.Action)
+ m.Group("/:username/:reponame", func() {
+ m.Get("/issues", repo.RetrieveLabels, repo.Issues)
+ m.Get("/issues/:index", repo.ViewIssue)
+ m.Get("/labels/", repo.RetrieveLabels, repo.Labels)
+ m.Get("/milestones", repo.Milestones)
+ }, ignSignIn, context.RepoAssignment(true))
+ m.Group("/:username/:reponame", func() {
+ // FIXME: should use different URLs but mostly same logic for comments of issue and pull reuqest.
+ // So they can apply their own enable/disable logic on routers.
+ m.Group("/issues", func() {
+ m.Combo("/new", repo.MustEnableIssues).Get(context.RepoRef(), repo.NewIssue).
+ Post(bindIgnErr(form.NewIssue{}), repo.NewIssuePost)
+
+ m.Group("/:index", func() {
+ m.Post("/title", repo.UpdateIssueTitle)
+ m.Post("/content", repo.UpdateIssueContent)
+ m.Combo("/comments").Post(bindIgnErr(form.CreateComment{}), repo.NewComment)
+ })
+ })
+ m.Group("/comments/:id", func() {
+ m.Post("", repo.UpdateCommentContent)
+ m.Post("/delete", repo.DeleteComment)
+ })
+ }, reqSignIn, context.RepoAssignment(true))
+ m.Group("/:username/:reponame", func() {
+ m.Group("/wiki", func() {
+ m.Get("/?:page", repo.Wiki)
+ m.Get("/_pages", repo.WikiPages)
+ }, repo.MustEnableWiki, context.RepoRef())
+ }, ignSignIn, context.RepoAssignment(false, true))
+
+ m.Group("/:username/:reponame", func() {
+ // FIXME: should use different URLs but mostly same logic for comments of issue and pull reuqest.
+ // So they can apply their own enable/disable logic on routers.
+ m.Group("/issues", func() {
+ m.Group("/:index", func() {
+ m.Post("/label", repo.UpdateIssueLabel)
+ m.Post("/milestone", repo.UpdateIssueMilestone)
+ m.Post("/assignee", repo.UpdateIssueAssignee)
+ }, reqRepoWriter)
+ })
+ m.Group("/labels", func() {
+ m.Post("/new", bindIgnErr(form.CreateLabel{}), repo.NewLabel)
+ m.Post("/edit", bindIgnErr(form.CreateLabel{}), repo.UpdateLabel)
+ m.Post("/delete", repo.DeleteLabel)
+ m.Post("/initialize", bindIgnErr(form.InitializeLabels{}), repo.InitializeLabels)
+ }, reqRepoWriter, context.RepoRef())
+ m.Group("/milestones", func() {
+ m.Combo("/new").Get(repo.NewMilestone).
+ Post(bindIgnErr(form.CreateMilestone{}), repo.NewMilestonePost)
+ m.Get("/:id/edit", repo.EditMilestone)
+ m.Post("/:id/edit", bindIgnErr(form.CreateMilestone{}), repo.EditMilestonePost)
+ m.Get("/:id/:action", repo.ChangeMilestonStatus)
+ m.Post("/delete", repo.DeleteMilestone)
+ }, reqRepoWriter, context.RepoRef())
+
+ m.Group("/releases", func() {
+ m.Get("/new", repo.NewRelease)
+ m.Post("/new", bindIgnErr(form.NewRelease{}), repo.NewReleasePost)
+ m.Post("/delete", repo.DeleteRelease)
+ m.Get("/edit/*", repo.EditRelease)
+ m.Post("/edit/*", bindIgnErr(form.EditRelease{}), repo.EditReleasePost)
+ }, repo.MustBeNotBare, reqRepoWriter, func(c *context.Context) {
+ c.Data["PageIsViewFiles"] = true
+ })
+
+ // FIXME: Should use c.Repo.PullRequest to unify template, currently we have inconsistent URL
+ // for PR in same repository. After select branch on the page, the URL contains redundant head user name.
+ // e.g. /org1/test-repo/compare/master...org1:develop
+ // which should be /org1/test-repo/compare/master...develop
+ m.Combo("/compare/*", repo.MustAllowPulls).Get(repo.CompareAndPullRequest).
+ Post(bindIgnErr(form.NewIssue{}), repo.CompareAndPullRequestPost)
+
+ m.Group("", func() {
+ m.Combo("/_edit/*").Get(repo.EditFile).
+ Post(bindIgnErr(form.EditRepoFile{}), repo.EditFilePost)
+ m.Combo("/_new/*").Get(repo.NewFile).
+ Post(bindIgnErr(form.EditRepoFile{}), repo.NewFilePost)
+ m.Post("/_preview/*", bindIgnErr(form.EditPreviewDiff{}), repo.DiffPreviewPost)
+ m.Combo("/_delete/*").Get(repo.DeleteFile).
+ Post(bindIgnErr(form.DeleteRepoFile{}), repo.DeleteFilePost)
+
+ m.Group("", func() {
+ m.Combo("/_upload/*").Get(repo.UploadFile).
+ Post(bindIgnErr(form.UploadRepoFile{}), repo.UploadFilePost)
+ m.Post("/upload-file", repo.UploadFileToServer)
+ m.Post("/upload-remove", bindIgnErr(form.RemoveUploadFile{}), repo.RemoveUploadFileFromServer)
+ }, func(c *context.Context) {
+ if !setting.Repository.Upload.Enabled {
+ c.NotFound()
+ return
+ }
+ })
+ }, repo.MustBeNotBare, reqRepoWriter, context.RepoRef(), func(c *context.Context) {
+ if !c.Repo.CanEnableEditor() {
+ c.NotFound()
+ return
+ }
+
+ c.Data["PageIsViewFiles"] = true
+ })
+ }, reqSignIn, context.RepoAssignment())
+
+ m.Group("/:username/:reponame", func() {
+ m.Group("", func() {
+ m.Get("/releases", repo.MustBeNotBare, repo.Releases)
+ m.Get("/pulls", repo.RetrieveLabels, repo.Pulls)
+ m.Get("/pulls/:index", repo.ViewPull)
+ }, context.RepoRef())
+
+ m.Group("/branches", func() {
+ m.Get("", repo.Branches)
+ m.Get("/all", repo.AllBranches)
+ m.Post("/delete/*", reqSignIn, reqRepoWriter, repo.DeleteBranchPost)
+ }, repo.MustBeNotBare, func(c *context.Context) {
+ c.Data["PageIsViewFiles"] = true
+ })
+
+ m.Group("/wiki", func() {
+ m.Group("", func() {
+ m.Combo("/_new").Get(repo.NewWiki).
+ Post(bindIgnErr(form.NewWiki{}), repo.NewWikiPost)
+ m.Combo("/:page/_edit").Get(repo.EditWiki).
+ Post(bindIgnErr(form.NewWiki{}), repo.EditWikiPost)
+ m.Post("/:page/delete", repo.DeleteWikiPagePost)
+ }, reqSignIn, reqRepoWriter)
+ }, repo.MustEnableWiki, context.RepoRef())
+
+ m.Get("/archive/*", repo.MustBeNotBare, repo.Download)
+
+ m.Group("/pulls/:index", func() {
+ m.Get("/commits", context.RepoRef(), repo.ViewPullCommits)
+ m.Get("/files", context.RepoRef(), repo.ViewPullFiles)
+ m.Post("/merge", reqRepoWriter, repo.MergePullRequest)
+ }, repo.MustAllowPulls)
+
+ m.Group("", func() {
+ m.Get("/src/*", repo.Home)
+ m.Get("/raw/*", repo.SingleDownload)
+ m.Get("/commits/*", repo.RefCommits)
+ m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.Diff)
+ m.Get("/forks", repo.Forks)
+ }, repo.MustBeNotBare, context.RepoRef())
+ m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)", repo.MustBeNotBare, repo.RawDiff)
+
+ m.Get("/compare/:before([a-z0-9]{40})\\.\\.\\.:after([a-z0-9]{40})", repo.MustBeNotBare, context.RepoRef(), repo.CompareDiff)
+ }, ignSignIn, context.RepoAssignment())
+ m.Group("/:username/:reponame", func() {
+ m.Get("/stars", repo.Stars)
+ m.Get("/watchers", repo.Watchers)
+ }, ignSignIn, context.RepoAssignment(), context.RepoRef())
+
+ m.Group("/:username", func() {
+ m.Get("/:reponame", ignSignIn, context.RepoAssignment(), context.RepoRef(), repo.Home)
+
+ m.Group("/:reponame", func() {
+ m.Head("/tasks/trigger", repo.TriggerTask)
+ })
+ // Use the regexp to match the repository name
+ // Duplicated route to enable different ways of accessing same set of URLs,
+ // e.g. with or without ".git" suffix.
+ m.Group("/:reponame([\\d\\w-_\\.]+\\.git$)", func() {
+ m.Get("", ignSignIn, context.RepoAssignment(), context.RepoRef(), repo.Home)
+ m.Options("/*", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
+ m.Route("/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
+ })
+ m.Options("/:reponame/*", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
+ m.Route("/:reponame/*", "GET,POST", ignSignInAndCsrf, repo.HTTPContexter(), repo.HTTP)
+ })
+ // ***** END: Repository *****
+
+ m.Group("/api", func() {
+ apiv1.RegisterRoutes(m)
+ }, ignSignIn)
+
+ m.Group("/-", func() {
+ if setting.Prometheus.Enabled {
+ m.Get("/metrics", func(c *context.Context) {
+ if !setting.Prometheus.EnableBasicAuth {
+ return
+ }
+
+ c.RequireBasicAuth(setting.Prometheus.BasicAuthUsername, setting.Prometheus.BasicAuthPassword)
+ }, promhttp.Handler())
+ }
+ })
+
+ // robots.txt
+ m.Get("/robots.txt", func(c *context.Context) {
+ if setting.HasRobotsTxt {
+ c.ServeFileContent(path.Join(setting.CustomPath, "robots.txt"))
+ } else {
+ c.NotFound()
+ }
+ })
+
+ // Not found handler.
+ m.NotFound(route.NotFound)
+
+ // Flag for port number in case first time run conflict.
+ if c.IsSet("port") {
+ setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, c.String("port"), 1)
+ setting.HTTPPort = c.String("port")
+ }
+
+ var listenAddr string
+ if setting.Protocol == setting.SCHEME_UNIX_SOCKET {
+ listenAddr = fmt.Sprintf("%s", setting.HTTPAddr)
+ } else {
+ listenAddr = fmt.Sprintf("%s:%s", setting.HTTPAddr, setting.HTTPPort)
+ }
+ log.Info("Listen: %v://%s%s", setting.Protocol, listenAddr, setting.AppSubURL)
+
+ var err error
+ switch setting.Protocol {
+ case setting.SCHEME_HTTP:
+ err = http.ListenAndServe(listenAddr, m)
+ case setting.SCHEME_HTTPS:
+ var tlsMinVersion uint16
+ switch setting.TLSMinVersion {
+ case "SSL30":
+ tlsMinVersion = tls.VersionSSL30
+ case "TLS12":
+ tlsMinVersion = tls.VersionTLS12
+ case "TLS11":
+ tlsMinVersion = tls.VersionTLS11
+ case "TLS10":
+ fallthrough
+ default:
+ tlsMinVersion = tls.VersionTLS10
+ }
+ server := &http.Server{Addr: listenAddr, TLSConfig: &tls.Config{
+ MinVersion: tlsMinVersion,
+ CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
+ PreferServerCipherSuites: true,
+ CipherSuites: []uint16{
+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // Required for HTTP/2 support.
+ tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+ },
+ }, Handler: m}
+ err = server.ListenAndServeTLS(setting.CertFile, setting.KeyFile)
+ case setting.SCHEME_FCGI:
+ err = fcgi.Serve(nil, m)
+ case setting.SCHEME_UNIX_SOCKET:
+ os.Remove(listenAddr)
+
+ var listener *net.UnixListener
+ listener, err = net.ListenUnix("unix", &net.UnixAddr{listenAddr, "unix"})
+ if err != nil {
+ break // Handle error after switch
+ }
+
+ // FIXME: add proper implementation of signal capture on all protocols
+ // execute this on SIGTERM or SIGINT: listener.Close()
+ if err = os.Chmod(listenAddr, os.FileMode(setting.UnixSocketPermission)); err != nil {
+ log.Fatal(4, "Failed to set permission of unix socket: %v", err)
+ }
+ err = http.Serve(listener, m)
+ default:
+ log.Fatal(4, "Invalid protocol: %s", setting.Protocol)
+ }
+
+ if err != nil {
+ log.Fatal(4, "Failed to start server: %v", err)
+ }
+
+ return nil
+}