Преглед изворни кода

Merge pull request #108 from mpsonntag/unlisted

Unlisted supported, dav removal, live cherry-picks

LGTM
Achilleas Koutsou пре 4 година
родитељ
комит
2efac57ed5
45 измењених фајлова са 330 додато и 616 уклоњено
  1. 1 1
      .github/ISSUE_TEMPLATE/security.md
  2. 4 2
      conf/locale/locale_en-US.ini
  3. 7 7
      internal/assets/conf/conf_gen.go
  4. 6 4
      internal/assets/public/public_gen.go
  5. 14 14
      internal/assets/templates/templates_gen.go
  6. 1 3
      internal/cmd/serv.go
  7. 0 9
      internal/cmd/web.go
  8. 0 2
      internal/conf/conf.go
  9. 0 6
      internal/conf/static.go
  10. 0 364
      internal/dav/dav.go
  11. 0 100
      internal/dav/middle.go
  12. 6 6
      internal/db/action.go
  13. 67 20
      internal/db/db_gin.go
  14. 135 0
      internal/db/db_gin_test.go
  15. 1 1
      internal/db/org.go
  16. 11 4
      internal/db/repo.go
  17. 1 34
      internal/db/user.go
  18. 3 1
      internal/form/repo.go
  19. 2 2
      internal/markup/odml.go
  20. 1 1
      internal/route/api/v1/misc/client.go
  21. 5 2
      internal/route/api/v1/repo/repo.go
  22. 1 1
      internal/route/api/v1/search/general.go
  23. 1 1
      internal/route/api/v1/search/suggest.go
  24. 1 0
      internal/route/repo/pull.go
  25. 2 0
      internal/route/repo/repo.go
  26. 3 2
      internal/route/repo/setting.go
  27. 1 1
      internal/route/routes_gin.go
  28. 2 11
      internal/route/search_gin.go
  29. 1 1
      internal/route/suggest.go
  30. 1 1
      public/css/custom.css
  31. 0 0
      public/css/gogs.min.css.map
  32. 0 0
      public/img/elife-logo-xs.fd623d00.svg
  33. BIN
      public/img/lmu.png
  34. BIN
      public/img/re3data_logo.png
  35. 1 0
      templates/admin/repo/list.tmpl
  36. 6 2
      templates/base/footer.tmpl
  37. 2 0
      templates/explore/repo_list.tmpl
  38. 2 0
      templates/mail/issue/comment.tmpl
  39. 2 0
      templates/mail/issue/mention.tmpl
  40. 3 1
      templates/mail/notify/collaborator.tmpl
  41. 7 0
      templates/repo/create.tmpl
  42. 2 2
      templates/repo/header.tmpl
  43. 7 0
      templates/repo/migrate.tmpl
  44. 7 0
      templates/repo/pulls/fork.tmpl
  45. 13 10
      templates/repo/settings/options.tmpl

+ 1 - 1
.github/ISSUE_TEMPLATE/security.md

@@ -7,6 +7,6 @@ about: Report security vulnerability for this project
 <!--
 
 Please create a dummy issue with high-level description of the security vulnerability,
-then report details to Achilleas Koutsou <koutsou@bio.lmu.com> privately, thank you!
+then report details to the G-Node team at <gin@g-node.org>, thank you!
 
 -->

+ 4 - 2
conf/locale/locale_en-US.ini

@@ -404,8 +404,10 @@ delete_account_desc = This account is going to be deleted permanently, do you wa
 owner = Owner
 repo_name = Repository Name
 repo_name_helper = Will be used to define the URL (path) of the repository. It must be one word (no spaces) and can contain letters (a-z, A-Z), numbers (0-9), dash (-), underscore (_), or dot (.) characters. Repository names should be short, unique and specific (do not use a generic name like "dataset", "plos_paper", etc).
-visibility = Private
-visiblity_helper = Accessible only to owner and assigned collaborators
+visibility = Visibility
+unlisted = Unlisted
+visiblity_helper = This repository is <span class="ui red text">Private</span>; accessible only to owner and assigned collaborators
+unlisted_helper = This repository is <span class="ui red text">Unlisted</span>
 visiblity_helper_forced = New repositories are private by default. You can change the state later.
 visiblity_fork_helper = (Change of this value will affect all forks)
 license_warn = Before making this public: Have you added a LICENSE file?

Разлика између датотеке није приказан због своје велике величине
+ 7 - 7
internal/assets/conf/conf_gen.go


Разлика између датотеке није приказан због своје велике величине
+ 6 - 4
internal/assets/public/public_gen.go


Разлика између датотеке није приказан због своје велике величине
+ 14 - 14
internal/assets/templates/templates_gen.go


+ 1 - 3
internal/cmd/serv.go

@@ -293,7 +293,7 @@ func runServ(c *cli.Context) error {
 
 // GIN specific: code altered from upstream, this function requires a review
 func runGit(cmd []string, requestMode db.AccessMode, user *db.User, owner *db.User,
-	repo *db.Repository) error {
+	repo *db.Repository) {
 	log.Info("Running %q", cmd)
 	gitCmd := exec.Command(cmd[0], cmd[1:]...)
 	if requestMode == db.AccessModeWrite {
@@ -313,8 +313,6 @@ func runGit(cmd []string, requestMode db.AccessMode, user *db.User, owner *db.Us
 	if err := gitCmd.Run(); err != nil {
 		fail("Internal error", "Failed to execute git command: %v", err)
 	}
-
-	return nil
 }
 
 // Make sure git-annex-shell does not make "bad" changes (refactored from repo)

+ 0 - 9
internal/cmd/web.go

@@ -34,7 +34,6 @@ import (
 	"github.com/G-Node/gogs/internal/assets/templates"
 	"github.com/G-Node/gogs/internal/conf"
 	"github.com/G-Node/gogs/internal/context"
-	"github.com/G-Node/gogs/internal/dav"
 	"github.com/G-Node/gogs/internal/db"
 	"github.com/G-Node/gogs/internal/form"
 	"github.com/G-Node/gogs/internal/osutil"
@@ -47,7 +46,6 @@ import (
 	"github.com/G-Node/gogs/internal/route/repo"
 	"github.com/G-Node/gogs/internal/route/user"
 	"github.com/G-Node/gogs/internal/template"
-	"golang.org/x/net/webdav"
 )
 
 var Web = cli.Command{
@@ -155,11 +153,6 @@ func newMacaron() *macaron.Macaron {
 		},
 	}))
 
-	// GIN specifc code: Webdav handler todo: implement
-	h := &webdav.Handler{FileSystem: &dav.GinFS{BasePath: conf.Repository.Root}, LockSystem: webdav.NewMemLS(),
-		Logger: dav.Logger}
-	m.Map(h)
-
 	return m
 }
 
@@ -422,8 +415,6 @@ func runWeb(c *cli.Context) error {
 			m.Combo("/fork/:repoid").Get(repo.Fork).
 				Post(bindIgnErr(form.CreateRepo{}), repo.ForkPost)
 		}, reqSignIn)
-		m.Any("/:username/:reponame/_dav/*", dav.DavMiddle(), dav.Dav) // GIN specific code
-		m.Any("/:username/:reponame/_dav", dav.DavMiddle(), dav.Dav) // GIN specific code
 
 		m.Group("/:username/:reponame", func() {
 			m.Group("/settings", func() {

+ 0 - 2
internal/conf/conf.go

@@ -403,8 +403,6 @@ func Init(customConf string) error {
 		return errors.Wrap(err, "mapping [doi] section")
 	} else if err = File.Section("cliconfig").MapTo(&CLIConfig); err != nil {
 		return errors.Wrap(err, "mapping [cliconfig] section")
-	} else if err = File.Section("dav").MapTo(&WebDav); err != nil {
-		return errors.Wrap(err, "mapping [dav] section")
 	}
 
 	HasRobotsTxt = osutil.IsFile(filepath.Join(CustomDir(), "robots.txt"))

+ 0 - 6
internal/conf/static.go

@@ -378,12 +378,6 @@ var (
 	CLIConfig struct {
 		RSAHostKey string
 	}
-
-	WebDav struct {
-		On        bool
-		Logged    bool
-		AuthRealm string
-	}
 )
 
 type ServerOpts struct {

+ 0 - 364
internal/dav/dav.go

@@ -1,364 +0,0 @@
-package dav
-
-import (
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"regexp"
-	"strings"
-	"time"
-
-	"io/ioutil"
-
-	"github.com/G-Node/git-module"
-	"github.com/G-Node/gogs/internal/conf"
-	gctx "github.com/G-Node/gogs/internal/context"
-	"github.com/G-Node/gogs/internal/db"
-	"github.com/G-Node/gogs/internal/tool"
-	"github.com/G-Node/libgin/libgin/annex"
-	"golang.org/x/net/context"
-	"golang.org/x/net/webdav"
-	log "gopkg.in/clog.v1"
-)
-
-var (
-	RE_GETRNAME = regexp.MustCompile(`.+\/(.+)\/_dav`)
-	RE_GETROWN  = regexp.MustCompile(`\/(.+)\/.+\/_dav`)
-	RE_GETFPATH = regexp.MustCompile("/_dav/(.+)")
-)
-
-const ANNEXPEEKSIZE = 1024
-
-func Dav(c *gctx.Context, handler *webdav.Handler) {
-	if !conf.WebDav.On {
-		c.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-	if checkPerms(c) != nil {
-		Webdav401(c)
-		return
-	}
-	handler.ServeHTTP(c.Resp, c.Req.Request)
-	return
-}
-
-// GinFS implements webdav (it implements webdav.Habdler) read only access to a repository
-type GinFS struct {
-	BasePath string
-}
-
-// Mkdir not implemented: Just return an error. -> Read Only
-func (fs *GinFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
-	return fmt.Errorf("Mkdir not implemented for read only gin FS")
-}
-
-// RemoveAll not implemented: Just return an error. -> Read Only
-func (fs *GinFS) RemoveAll(ctx context.Context, name string) error {
-	return fmt.Errorf("RemoveAll not implemented for read only gin FS")
-}
-
-// Rename not implemented: Just return an error. -> Read Only
-func (fs *GinFS) Rename(ctx context.Context, oldName, newName string) error {
-	return fmt.Errorf("Rename not implemented for read only gin FS")
-}
-
-// OpenFile returns a named file from the repository
-func (fs *GinFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
-	//todo: catch all the errors
-	rname, err := getRName(name)
-	if err != nil {
-		return nil, err
-	}
-
-	oname, err := getOName(name)
-	if err != nil {
-		return nil, err
-	}
-
-	path, _ := getFPath(name)
-
-	rpath := fmt.Sprintf("%s/%s/%s.git", fs.BasePath, oname, rname)
-	grepo, err := git.OpenRepository(rpath)
-	if err != nil {
-		return nil, err
-	}
-	com, err := grepo.GetBranchCommit("master")
-	if err != nil {
-		return nil, err
-	}
-	tree, _ := com.SubTree(path)
-	trentry, _ := com.GetTreeEntryByPath(path)
-	return &GinFile{trentry: trentry, tree: tree, LChange: com.Committer.When, rpath: rpath}, nil
-}
-
-func (fs *GinFS) Stat(ctx context.Context, name string) (os.FileInfo, error) {
-	f, err := fs.OpenFile(ctx, name, 0, 0)
-	if err != nil {
-		return nil, err
-	}
-	return f.Stat()
-}
-
-type GinFile struct {
-	tree      *git.Tree
-	trentry   *git.TreeEntry
-	dirrcount int
-	seekoset  int64
-	LChange   time.Time
-	rpath     string
-	afp       *os.File
-}
-
-func (f *GinFile) Write(p []byte) (n int, err error) {
-	return 0, fmt.Errorf("write to GinFile not implemented (read only)")
-}
-
-func (f *GinFile) Close() error {
-	if f.afp != nil {
-		return f.afp.Close()
-	}
-	return nil
-}
-
-func (f *GinFile) read(p []byte) (int, error) {
-	if f.trentry == nil {
-		return 0, fmt.Errorf("File not found")
-	}
-	if f.trentry.Type != git.OBJECT_BLOB {
-		return 0, fmt.Errorf("not a blob")
-	}
-	data, err := f.trentry.Blob().Data()
-	if err != nil {
-		return 0, err
-	}
-	// todo: read with pipes
-	io.CopyN(ioutil.Discard, data, f.seekoset)
-	n, err := data.Read(p)
-	if err != nil {
-		return n, err
-	}
-	return n, nil
-}
-func (f *GinFile) Read(p []byte) (int, error) {
-	if f.afp != nil {
-		return f.afp.Read(p)
-	}
-	tmp := make([]byte, len(p))
-	n, err := f.read(tmp)
-	tmp = tmp[:n]
-	if err != nil {
-		return n, err
-	}
-
-	annexed := tool.IsAnnexedFile(tmp)
-	if annexed {
-		af, err := annex.NewAFile(f.rpath, "annex", f.trentry.Name(), tmp)
-		if err != nil {
-			return n, err
-		}
-		f.afp, _ = af.Open()
-		f.afp.Seek(f.seekoset, io.SeekStart)
-		return f.afp.Read(p)
-	}
-	copy(p, tmp)
-	f.Seek(int64(n), io.SeekCurrent)
-	return n, nil
-}
-
-func (f *GinFile) Seek(offset int64, whence int) (int64, error) {
-	if f.afp != nil {
-		return f.afp.Seek(offset, whence)
-	}
-	st, err := f.Stat()
-	if err != nil {
-		return f.seekoset, err
-	}
-	switch whence {
-	case io.SeekStart:
-		if offset > st.Size() || offset < 0 {
-			return 0, fmt.Errorf("cannot seek to %d, only %d big", offset, st.Size())
-		}
-		f.seekoset = offset
-		return f.seekoset, nil
-	case io.SeekCurrent:
-		noffset := f.seekoset + offset
-		if noffset > st.Size() || noffset < 0 {
-			return 0, fmt.Errorf("cannot seek to %d, only %d big", offset, st.Size())
-		}
-		f.seekoset = noffset
-		return f.seekoset, nil
-	case io.SeekEnd:
-		fsize := st.Size()
-		noffset := fsize - offset
-		if noffset > fsize || noffset < 0 {
-			return 0, fmt.Errorf("cannot seek to %d, only %d big", offset, st.Size())
-		}
-		f.seekoset = noffset
-		return f.seekoset, nil
-	}
-	return f.seekoset, fmt.Errorf("seeking failed")
-}
-
-func (f *GinFile) Readdir(count int) ([]os.FileInfo, error) {
-	ents, err := f.tree.ListEntries()
-	if err != nil {
-		return nil, err
-	}
-	// give back all the stuff
-	if count <= 0 {
-		return f.getFInfos(ents)
-	}
-	// user requested a bufferrd read
-	switch {
-	case count > len(ents):
-		infos, err := f.getFInfos(ents)
-		if err != nil {
-			return nil, err
-		}
-		return infos, io.EOF
-	case f.dirrcount >= len(ents):
-		return nil, io.EOF
-	case f.dirrcount+count >= len(ents):
-		infos, err := f.getFInfos(ents[f.dirrcount:])
-		if err != nil {
-			return nil, err
-		}
-		f.dirrcount = len(ents)
-		return infos, io.EOF
-	case f.dirrcount+count < len(ents):
-		infos, err := f.getFInfos(ents[f.dirrcount : f.dirrcount+count])
-		if err != nil {
-			return nil, err
-		}
-		f.dirrcount = f.dirrcount + count
-		return infos, nil
-	}
-	return nil, nil
-}
-
-func (f *GinFile) getFInfos(ents []*git.TreeEntry) ([]os.FileInfo, error) {
-	infos := make([]os.FileInfo, len(ents))
-	for c, ent := range ents {
-		finfo, err := GinFile{trentry: ent, rpath: f.rpath}.Stat()
-		if err != nil {
-			return nil, err
-		}
-		infos[c] = finfo
-	}
-	return infos, nil
-}
-func (f GinFile) Stat() (os.FileInfo, error) {
-	if f.trentry == nil {
-		return nil, fmt.Errorf("File not found")
-	}
-	if f.trentry.Type != git.OBJECT_BLOB {
-		return GinFinfo{TreeEntry: f.trentry, LChange: f.LChange}, nil
-	}
-	peek := make([]byte, ANNEXPEEKSIZE)
-	offset := f.seekoset
-	f.seekoset = 0
-	n, err := f.read(peek)
-	f.seekoset = offset
-	if err != nil {
-		return nil, err
-	}
-	peek = peek[:n]
-	if tool.IsAnnexedFile(peek) {
-		af, err := annex.NewAFile(f.rpath, "annex", f.trentry.Name(), peek)
-		if err != nil {
-			return nil, err
-		}
-		f.trentry.SetSize(af.Info.Size())
-	}
-	return GinFinfo{TreeEntry: f.trentry, LChange: f.LChange}, nil
-}
-
-type GinFinfo struct {
-	*git.TreeEntry
-	LChange time.Time
-}
-
-func (i GinFinfo) Mode() os.FileMode {
-	return 0
-}
-
-func (i GinFinfo) ModTime() time.Time {
-	return i.LChange
-}
-
-func (i GinFinfo) Sys() interface{} {
-	return nil
-}
-
-func checkPerms(c *gctx.Context) error {
-	if !c.Repo.HasAccess() {
-		return fmt.Errorf("no access")
-	}
-	if !conf.WebDav.Logged {
-		return nil
-	}
-	if !c.IsLogged {
-		return fmt.Errorf("no access")
-	}
-	return nil
-}
-
-func getRepo(path string) (*db.Repository, error) {
-	oID, err := getROwnerID(path)
-	if err != nil {
-		return nil, err
-	}
-
-	rname, err := getRName(path)
-	if err != nil {
-		return nil, err
-	}
-
-	return db.GetRepositoryByName(oID, rname)
-}
-
-func getRName(path string) (string, error) {
-	name := RE_GETRNAME.FindStringSubmatch(path)
-	if len(name) > 1 {
-		return strings.ToLower(name[1]), nil
-	}
-	return "", fmt.Errorf("could not determine repo name")
-}
-
-func getOName(path string) (string, error) {
-	name := RE_GETROWN.FindStringSubmatch(path)
-	if len(name) > 1 {
-		return strings.ToLower(name[1]), nil
-	}
-	return "", fmt.Errorf("could not determine repo owner")
-}
-
-func getFPath(path string) (string, error) {
-	name := RE_GETFPATH.FindStringSubmatch(path)
-	if len(name) > 1 {
-		return name[1], nil
-	}
-	return "", fmt.Errorf("could not determine file path from %s", name)
-}
-
-func getROwnerID(path string) (int64, error) {
-	name := RE_GETROWN.FindStringSubmatch(path)
-	if len(name) > 1 {
-		db.GetUserByName(name[1])
-	}
-	return -100, fmt.Errorf("could not determine repo owner")
-}
-
-func Webdav401(c *gctx.Context) {
-	c.Header().Add("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", conf.WebDav.AuthRealm))
-	c.WriteHeader(http.StatusUnauthorized)
-	return
-}
-
-func Logger(req *http.Request, err error) {
-	if err != nil {
-		log.Info("davlog: err:%+v", err)
-		log.Trace("davlog: req:%+v", req)
-	}
-}

+ 0 - 100
internal/dav/middle.go

@@ -1,100 +0,0 @@
-package dav
-
-import (
-	"net/http"
-	"strings"
-
-	"github.com/G-Node/gogs/internal/context"
-	"github.com/G-Node/gogs/internal/db"
-	"github.com/gogs/git-module"
-	"gopkg.in/macaron.v1"
-)
-
-// DavMiddle initialises and returns a WebDav middleware handler (macaron.Handler)
-// [0]: issues, [1]: wiki
-func DavMiddle() macaron.Handler {
-	return func(c *context.Context) {
-		var (
-			owner *db.User
-			err   error
-		)
-
-		ownerName := c.Params(":username")
-		repoName := strings.TrimSuffix(c.Params(":reponame"), ".git")
-
-		// Check if the user is the same as the repository owner
-		if c.IsLogged && c.User.LowerName == strings.ToLower(ownerName) {
-			owner = c.User
-		} else {
-			owner, err = db.GetUserByName(ownerName)
-			if err != nil {
-				Webdav401(c)
-				return
-			}
-		}
-		c.Repo.Owner = owner
-
-		repo, err := db.GetRepositoryByName(owner.ID, repoName)
-		if err != nil {
-			Webdav401(c)
-			return
-		}
-
-		c.Repo.Repository = repo
-		c.Repo.RepoLink = repo.Link()
-
-		// Admin has super access.
-		if c.IsLogged && c.User.IsAdmin {
-			c.Repo.AccessMode = db.AccessModeOwner
-		} else {
-			mode, err := db.UserAccessMode(c.UserID(), repo)
-			if err != nil {
-				c.WriteHeader(http.StatusInternalServerError)
-				return
-			}
-			c.Repo.AccessMode = mode
-		}
-
-		if repo.IsMirror {
-			c.Repo.Mirror, err = db.GetMirrorByRepoID(repo.ID)
-			if err != nil {
-				c.WriteHeader(http.StatusInternalServerError)
-				return
-			}
-		}
-
-		gitRepo, err := git.Open(db.RepoPath(ownerName, repoName))
-		if err != nil {
-			c.WriteHeader(http.StatusInternalServerError)
-			return
-		}
-		c.Repo.GitRepo = gitRepo
-
-		tags, err := c.Repo.GitRepo.Tags()
-		if err != nil {
-			c.WriteHeader(http.StatusInternalServerError)
-			return
-		}
-		c.Repo.Repository.NumTags = len(tags)
-
-		// repo is bare and display enable
-		if c.Repo.Repository.IsBare {
-			return
-		}
-
-		brs, err := c.Repo.GitRepo.Branches()
-		if err != nil {
-			c.WriteHeader(http.StatusInternalServerError)
-			return
-		}
-		// If not branch selected, try default one.
-		// If default branch doesn't exists, fall back to some other branch.
-		if len(c.Repo.BranchName) == 0 {
-			if len(c.Repo.Repository.DefaultBranch) > 0 && gitRepo.HasBranch(c.Repo.Repository.DefaultBranch) {
-				c.Repo.BranchName = c.Repo.Repository.DefaultBranch
-			} else if len(brs) > 0 {
-				c.Repo.BranchName = brs[0]
-			}
-		}
-	}
-}

+ 6 - 6
internal/db/action.go

@@ -188,7 +188,7 @@ func newRepoAction(e Engine, doer, owner *User, repo *Repository) (err error) {
 		RepoID:       repo.ID,
 		RepoUserName: repo.Owner.Name,
 		RepoName:     repo.Name,
-		IsPrivate:    repo.IsPrivate,
+		IsPrivate:    repo.IsPrivate || repo.IsUnlisted,
 	})
 }
 
@@ -205,7 +205,7 @@ func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Reposit
 		RepoID:       repo.ID,
 		RepoUserName: repo.Owner.Name,
 		RepoName:     repo.Name,
-		IsPrivate:    repo.IsPrivate,
+		IsPrivate:    repo.IsPrivate || repo.IsUnlisted,
 		Content:      oldRepoName,
 	}); err != nil {
 		return fmt.Errorf("notify watchers: %v", err)
@@ -512,7 +512,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 		RepoUserName: repo.MustOwner().Name,
 		RepoName:     repo.Name,
 		RefName:      refName,
-		IsPrivate:    repo.IsPrivate,
+		IsPrivate:    repo.IsPrivate || repo.IsUnlisted,
 	}
 
 	apiRepo := repo.APIFormat(nil)
@@ -628,7 +628,7 @@ func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err e
 		RepoID:       repo.ID,
 		RepoUserName: repo.Owner.Name,
 		RepoName:     repo.Name,
-		IsPrivate:    repo.IsPrivate,
+		IsPrivate:    repo.IsPrivate || repo.IsUnlisted,
 		Content:      path.Join(oldOwner.Name, repo.Name),
 	}); err != nil {
 		return fmt.Errorf("notifyWatchers: %v", err)
@@ -659,7 +659,7 @@ func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue
 		RepoID:       repo.ID,
 		RepoUserName: repo.Owner.Name,
 		RepoName:     repo.Name,
-		IsPrivate:    repo.IsPrivate,
+		IsPrivate:    repo.IsPrivate || repo.IsUnlisted,
 	})
 }
 
@@ -678,7 +678,7 @@ func mirrorSyncAction(opType ActionType, repo *Repository, refName string, data
 		RepoUserName: repo.MustOwner().Name,
 		RepoName:     repo.Name,
 		RefName:      refName,
-		IsPrivate:    repo.IsPrivate,
+		IsPrivate:    repo.IsPrivate || repo.IsUnlisted,
 	})
 }
 

+ 67 - 20
internal/db/models_gin.go → internal/db/db_gin.go

@@ -1,17 +1,21 @@
 package db
 
 import (
+	"bufio"
 	"encoding/json"
 	"fmt"
 	"net/http"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 
+	"github.com/G-Node/git-module"
 	"github.com/G-Node/gogs/internal/conf"
 	"github.com/G-Node/libgin/libgin"
 	"github.com/G-Node/libgin/libgin/annex"
-	"github.com/gogs/git-module"
+	"github.com/unknwon/com"
+	"golang.org/x/crypto/bcrypt"
 	log "gopkg.in/clog.v1"
 )
 
@@ -130,29 +134,12 @@ func annexSetup(path string) {
 	}
 }
 
-func annexSync(path string) error {
-	log.Trace("Synchronising annexed data")
-	if msg, err := annex.ASync(path, "--content"); err != nil {
-		// TODO: This will also DOWNLOAD content, which is unnecessary for a simple upload
-		// TODO: Use gin-cli upload function instead
-		log.Error(2, "Annex sync failed: %v (%s)", err, msg)
-		return fmt.Errorf("git annex sync --content [%s]", path)
-	}
-
-	// run twice; required if remote annex is not initialised
-	if msg, err := annex.ASync(path, "--content"); err != nil {
-		log.Error(2, "Annex sync failed: %v (%s)", err, msg)
-		return fmt.Errorf("git annex sync --content [%s]", path)
-	}
-	return nil
-}
-
 func annexAdd(repoPath string, all bool, files ...string) error {
 	cmd := git.NewCommand("annex", "add")
 	if all {
-		cmd.AddArgs(".")
+		cmd.AddArguments(".")
 	}
-	_, err := cmd.AddArgs(files...).RunInDir(repoPath)
+	_, err := cmd.AddArguments(files...).RunInDir(repoPath)
 	return err
 }
 
@@ -169,3 +156,63 @@ func annexUpload(repoPath, remote string) error {
 	}
 	return nil
 }
+
+// isAddressAllowed returns true if the email address is allowed to sign up
+// based on the regular expressions found in the email filter file
+// (custom/addressfilters).
+// In case of errors (opening or reading file) or no matches, the function
+// defaults to 'true'.
+func isAddressAllowed(email string) bool {
+	fpath := filepath.Join(conf.CustomDir(), "addressfilters")
+	if !com.IsExist(fpath) {
+		// file doesn't exist: default allow everything
+		return true
+	}
+
+	f, err := os.Open(fpath)
+	if err != nil {
+		log.Error(2, "Failed to open file %q: %v", fpath, err)
+		// file read error: default allow everything
+		return true
+	}
+	defer f.Close()
+
+	emailBytes := []byte(email)
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		// Check provided email address against each line regex
+		// Failure to match any line returns true (allowed)
+		// Matching a line prefixed with + returns true (allowed)
+		// Matching a line prefixed with - returns false (blocked)
+		// Erroneous patterns are logged and ignored
+		var allow bool
+		line := scanner.Text()
+		if line[0] == '-' {
+			allow = false
+		} else if line[0] == '+' {
+			allow = true
+		} else {
+			log.Error(2, "Invalid line in addressfilters: %s", line)
+			log.Error(2, "Prefix invalid (must be '-' or '+')")
+			continue
+		}
+		pattern := strings.TrimSpace(line[1:])
+		match, err := regexp.Match(pattern, emailBytes)
+		if err != nil {
+			log.Error(2, "Invalid line in addressfilters: %s", line)
+			log.Error(2, "Invalid pattern: %v", err)
+		}
+		if match {
+			log.Trace("New user email %q matched filter rule %q (Allow: %t)", email, line, allow)
+			return allow
+		}
+	}
+
+	// No match: Default to allow
+	return true
+}
+
+func (u *User) OldGinVerifyPassword(plain string) bool {
+	err := bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(plain))
+	return err == nil
+}

+ 135 - 0
internal/db/db_gin_test.go

@@ -0,0 +1,135 @@
+package db
+
+import (
+	"io/ioutil"
+	"math/rand"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/G-Node/gogs/internal/conf"
+)
+
+const ALNUM = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+var emails = []string{
+	"foo@example.org",
+	"spammer@example.com",
+	"user@malicious-domain.net",
+	"someone@example.mal",
+}
+
+var blockEverythingFilter = []string{
+	"- .*",
+}
+
+var allowEverythingFilter = []string{
+	"+ .*",
+}
+
+var allowGNodeFilter = []string{
+	"+ @g-node.org$",
+	"- .*",
+}
+
+var blockMalicious = []string{
+	"- .*malicious-domain.net$",
+	"- spammer@",
+}
+
+func filterExpect(t *testing.T, address string, expect bool) {
+	if isAddressAllowed(address) != expect {
+		t.Fatalf("Address %q block: expected %t got %t", address, expect, !expect)
+	}
+}
+
+// Writes the filters to a file in the specified custom directory. This file needs to
+// be cleaned up afterwards. Returns the full path to the written file as string.
+func writeCustomDirFilterFile(t *testing.T, filters []string) string {
+	fname := filepath.Join(conf.CustomDir(), "addressfilters")
+
+	if err := ioutil.WriteFile(fname, []byte(strings.Join(filters, "\n")), 0777); err != nil {
+		t.Fatalf("Failed to write line filters to file %q: %v", fname, err.Error())
+	}
+	return fname
+}
+
+// randAlnum returns a random alphanumeric (lowercase, latin) string of length 'n'.
+func randAlnum(n int) string {
+	N := len(ALNUM)
+	chrs := make([]byte, n)
+	for idx := range chrs {
+		chrs[idx] = ALNUM[rand.Intn(N)]
+	}
+
+	return string(chrs)
+}
+
+func randAddress() string {
+	user := randAlnum(rand.Intn(20))
+	domain := randAlnum(rand.Intn(20)) + "." + randAlnum(rand.Intn(3))
+
+	return string(user) + "@" + string(domain)
+}
+
+func TestAllowGNodeFilter(t *testing.T) {
+	cdir := filepath.Join(conf.CustomDir())
+	if _, err := os.Stat(cdir); os.IsNotExist(err) {
+		_ = os.Mkdir(cdir, 0777)
+	}
+
+	ffile := writeCustomDirFilterFile(t, allowGNodeFilter)
+	defer os.Remove(ffile)
+
+	for _, address := range emails {
+		filterExpect(t, address, false)
+	}
+
+	filterExpect(t, "me@g-node.org", true)
+	filterExpect(t, "malicious@g-node.org@piracy.tk", false)
+}
+
+func TestEverythingFilters(t *testing.T) {
+	cdir := filepath.Join(conf.CustomDir())
+	if _, err := os.Stat(cdir); os.IsNotExist(err) {
+		_ = os.Mkdir(cdir, 0777)
+	}
+
+	ffile := writeCustomDirFilterFile(t, allowEverythingFilter)
+	defer os.Remove(ffile)
+
+	rand.Seed(time.Now().UnixNano())
+
+	for idx := 0; idx < 100; idx++ {
+		filterExpect(t, randAddress(), true)
+	}
+
+	ffile = writeCustomDirFilterFile(t, blockEverythingFilter)
+	defer os.Remove(ffile)
+
+	for idx := 0; idx < 100; idx++ {
+		filterExpect(t, randAddress(), false)
+	}
+}
+
+func TestBlockDomainFilter(t *testing.T) {
+	cdir := filepath.Join(conf.CustomDir())
+	if _, err := os.Stat(cdir); os.IsNotExist(err) {
+		_ = os.Mkdir(cdir, 0777)
+	}
+
+	ffile := writeCustomDirFilterFile(t, blockMalicious)
+	defer os.Remove(ffile)
+
+	// 0, 3 should be allowed; 1, 2 should be blocked
+	filterExpect(t, emails[0], true)
+	filterExpect(t, emails[1], false)
+	filterExpect(t, emails[2], false)
+	filterExpect(t, emails[3], true)
+}
+
+func TestFiltersNone(t *testing.T) {
+	filterExpect(t, emails[3], true)
+}

+ 1 - 1
internal/db/org.go

@@ -517,7 +517,7 @@ func (org *User) GetUserRepositories(userID int64, page, pageSize int) ([]*Repos
 	repos := make([]*Repository, 0, pageSize)
 	if err = x.Where("owner_id = ?", org.ID).
 		And(builder.Or(
-			builder.Expr("is_private = ?", false),
+			builder.And(builder.Expr("is_private = ?", false), builder.Expr("is_unlisted = ?", false)),
 			builder.In("id", teamRepoIDs))).
 		Desc("updated_unix").
 		Limit(pageSize, (page-1)*pageSize).

+ 11 - 4
internal/db/repo.go

@@ -174,8 +174,8 @@ type Repository struct {
 	NumTags             int `xorm:"-" gorm:"-" json:"-"`
 
 	IsPrivate bool
+	IsUnlisted  bool
 	IsBare    bool
-	Unlisted  bool
 
 	IsMirror bool
 	*Mirror  `xorm:"-" gorm:"-" json:"-"`
@@ -725,6 +725,7 @@ type MigrateRepoOptions struct {
 	Name        string
 	Description string
 	IsPrivate   bool
+	IsUnlisted  bool
 	IsMirror    bool
 	RemoteAddr  string
 }
@@ -754,6 +755,7 @@ func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository,
 		Name:        opts.Name,
 		Description: opts.Description,
 		IsPrivate:   opts.IsPrivate,
+		IsUnlisted:  opts.IsUnlisted,
 		IsMirror:    opts.IsMirror,
 	})
 	if err != nil {
@@ -928,6 +930,7 @@ type CreateRepoOptions struct {
 	License     string
 	Readme      string
 	IsPrivate   bool
+	IsUnlisted  bool
 	IsMirror    bool
 	AutoInit    bool
 }
@@ -1139,6 +1142,7 @@ func CreateRepository(doer, owner *User, opts CreateRepoOptions) (_ *Repository,
 		LowerName:    strings.ToLower(opts.Name),
 		Description:  opts.Description,
 		IsPrivate:    opts.IsPrivate,
+		IsUnlisted:   opts.IsUnlisted,
 		EnableWiki:   true,
 		EnableIssues: true,
 		EnablePulls:  true,
@@ -1486,13 +1490,14 @@ func updateRepository(e Engine, repo *Repository, visibilityChanged bool) (err e
 		}
 		for i := range forkRepos {
 			forkRepos[i].IsPrivate = repo.IsPrivate
+			forkRepos[i].IsUnlisted = repo.IsUnlisted
 			if err = updateRepository(e, forkRepos[i], true); err != nil {
 				return fmt.Errorf("updateRepository[%d]: %v", forkRepos[i].ID, err)
 			}
 		}
 
 		// Change visibility of generated actions
-		if _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&Action{IsPrivate: repo.IsPrivate}); err != nil {
+		if _, err = e.Where("repo_id = ?", repo.ID).Cols("is_private").Update(&Action{IsPrivate: repo.IsPrivate || repo.IsUnlisted}); err != nil {
 			return fmt.Errorf("change action visibility of repository: %v", err)
 		}
 	}
@@ -1695,6 +1700,7 @@ func GetUserRepositories(opts *UserRepoOptions) ([]*Repository, error) {
 	sess := x.Where("owner_id=?", opts.UserID).Desc("updated_unix")
 	if !opts.Private {
 		sess.And("is_private=?", false)
+		sess.And("is_unlisted=?", false)
 	}
 
 	if opts.Page <= 0 {
@@ -1768,11 +1774,11 @@ func SearchRepositoryByName(opts *SearchRepoOptions) (repos []*Repository, count
 	// this does not include other people's private repositories even if opts.UserID is an admin.
 	if !opts.Private && opts.UserID > 0 {
 		sess.Join("LEFT", "access", "access.repo_id = repo.id").
-			Where("repo.owner_id = ? OR access.user_id = ? OR repo.is_private = ? OR (repo.is_private = ? AND (repo.allow_public_wiki = ? OR repo.allow_public_issues = ?))", opts.UserID, opts.UserID, false, true, true, true)
+		Where("repo.owner_id = ? OR access.user_id = ? OR (repo.is_private = ? AND repo.is_unlisted = ?) OR (repo.is_private = ? AND (repo.allow_public_wiki = ? OR repo.allow_public_issues = ?))", opts.UserID, opts.UserID, false, false, true, true, true)
 	} else {
 		// Only return public repositories if opts.Private is not set
 		if !opts.Private {
-			sess.And("repo.is_private = ? OR (repo.is_private = ? AND (repo.allow_public_wiki = ? OR repo.allow_public_issues = ?))", false, true, true, true)
+			sess.And("(repo.is_private = ? AND repo.is_unlisted = ?) OR (repo.is_private = ? AND (repo.allow_public_wiki = ? OR repo.allow_public_issues = ?))", false, false, true, true, true)
 		}
 	}
 	if len(opts.Keyword) > 0 {
@@ -2408,6 +2414,7 @@ func ForkRepository(doer, owner *User, baseRepo *Repository, name, desc string)
 		Description:   desc,
 		DefaultBranch: baseRepo.DefaultBranch,
 		IsPrivate:     baseRepo.IsPrivate,
+		IsUnlisted:    baseRepo.IsUnlisted,
 		IsFork:        true,
 		ForkID:        baseRepo.ID,
 	}

+ 1 - 34
internal/db/user.go

@@ -5,7 +5,6 @@
 package db
 
 import (
-	"bufio"
 	"bytes"
 	"crypto/sha256"
 	"crypto/subtle"
@@ -15,7 +14,6 @@ import (
 	_ "image/jpeg"
 	"image/png"
 	"os"
-	"path"
 	"path/filepath"
 	"strings"
 	"time"
@@ -36,7 +34,6 @@ import (
 	"github.com/G-Node/gogs/internal/errutil"
 	"github.com/G-Node/gogs/internal/strutil"
 	"github.com/G-Node/gogs/internal/tool"
-	"golang.org/x/crypto/bcrypt"
 )
 
 // USER_AVATAR_URL_PREFIX is used to identify a URL is to access user avatar.
@@ -329,11 +326,6 @@ func (u *User) EncodePassword() {
 	u.Passwd = fmt.Sprintf("%x", newPasswd)
 }
 
-func (u *User) OldGinVerifyPassword(plain string) bool {
-	err := bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(plain))
-	return err == nil
-}
-
 // ValidatePassword checks if given password matches the one belongs to the user.
 func (u *User) ValidatePassword(passwd string) bool {
 	if u.OldGinVerifyPassword(passwd) {
@@ -494,31 +486,6 @@ func IsUserExist(uid int64, name string) (bool, error) {
 	return x.Where("id != ?", uid).Get(&User{LowerName: strings.ToLower(name)})
 }
 
-func IsBlockedDomain(email string) bool {
-	fpath := path.Join(conf.CustomDir(), "blocklist")
-	if !com.IsExist(fpath) {
-		return false
-	}
-
-	f, err := os.Open(fpath)
-	if err != nil {
-		log.Error("Failed to open file %q: %v", fpath, err)
-		return false
-	}
-	defer f.Close()
-
-	scanner := bufio.NewScanner(f)
-	for scanner.Scan() {
-		// Check provided email address against each line as suffix
-		if strings.HasSuffix(email, scanner.Text()) {
-			log.Trace("New user email matched blocked domain: %q", email)
-			return true
-		}
-	}
-
-	return false
-}
-
 // GetUserSalt returns a ramdom user salt token.
 func GetUserSalt() (string, error) {
 	return strutil.RandomChars(10)
@@ -617,7 +584,7 @@ func CreateUser(u *User) (err error) {
 		return ErrEmailAlreadyUsed{args: errutil.Args{"email": u.Email}}
 	}
 
-	if IsBlockedDomain(u.Email) {
+	if !isAddressAllowed(u.Email) {
 		return ErrBlockedDomain{u.Email}
 	}
 

+ 3 - 1
internal/form/repo.go

@@ -26,6 +26,7 @@ type CreateRepo struct {
 	UserID      int64  `binding:"Required"`
 	RepoName    string `binding:"Required;AlphaDashDot;MaxSize(100)"`
 	Private     bool
+	Unlisted    bool
 	Description string `binding:"MaxSize(512)"`
 	AutoInit    bool
 	Gitignores  string
@@ -45,6 +46,7 @@ type MigrateRepo struct {
 	RepoName     string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
 	Mirror       bool   `json:"mirror"`
 	Private      bool   `json:"private"`
+	Unlisted     bool   `json:"unlisted"`
 	Description  string `json:"description" binding:"MaxSize(512)"`
 }
 
@@ -92,7 +94,7 @@ type RepoSetting struct {
 	Interval      int
 	MirrorAddress string
 	Private       bool
-	Listed        bool
+	Unlisted      bool
 	EnablePrune   bool
 
 	// Advanced settings

+ 2 - 2
internal/markup/odml.go

@@ -14,7 +14,7 @@ func MarshalODML(buf []byte) []byte {
 	od := Odml{}
 	decoder := xml.NewDecoder(bytes.NewReader(buf))
 	decoder.CharsetReader = charset.NewReaderLabel
-	decoder.Decode(&od)
+	_ = decoder.Decode(&od)
 	data, _ := json.Marshal(od)
 	return data
 }
@@ -57,7 +57,7 @@ func (u *Property) MarshalJSON() ([]byte, error) {
 func (u *Section) MarshalJSON() ([]byte, error) {
 	type Alias Section
 	if u.Text == "" {
-		u.Text = fmt.Sprintf("%s", u.Name)
+		u.Text = u.Name
 	}
 	for _, x := range u.Properties {
 		u.Children = append(u.Children, OdMLObject{Prop: x, Type: "property"})

+ 1 - 1
internal/route/api/v1/misc/client.go

@@ -34,5 +34,5 @@ func ClientC(c *context.APIContext) {
 		return
 	}
 	c.WriteHeader(http.StatusOK)
-	c.Write(data)
+	_, _ = c.Write(data)
 }

+ 5 - 2
internal/route/api/v1/repo/repo.go

@@ -27,7 +27,8 @@ func Search(c *context.APIContext) {
 		Page:     c.QueryInt("page"),
 	}
 
-	// workaround for the all query with logged users
+	// GIN specific code
+	// Workaround for the all query with logged users (?)
 	if opts.Keyword == "." {
 		opts.Keyword = ""
 	}
@@ -68,9 +69,11 @@ func Search(c *context.APIContext) {
 		return
 	}
 
+	// GIN specific code
+	// 'for' has been modfied to accomodate search in commits as well (?)
 	results := make([]*api.Repository, 0, len(repos))
 	for i := range repos {
-		if !repos[i].Unlisted {
+		if !repos[i].IsUnlisted {
 			rep := repos[i].APIFormat(nil)
 			rep.Owner.Email = ""
 			results = append(results, rep)

+ 1 - 1
internal/route/api/v1/search/general.go

@@ -39,5 +39,5 @@ func Search(c *context.APIContext) {
 		c.Status(http.StatusInternalServerError)
 		return
 	}
-	c.Write(data)
+	_, _ = c.Write(data)
 }

+ 1 - 1
internal/route/api/v1/search/suggest.go

@@ -35,5 +35,5 @@ func Suggest(c *context.APIContext) {
 		c.Status(http.StatusInternalServerError)
 		return
 	}
-	c.Write(data)
+	_, _ = c.Write(data)
 }

+ 1 - 0
internal/route/repo/pull.go

@@ -60,6 +60,7 @@ func parseBaseRepository(c *context.Context) *db.Repository {
 	c.Data["repo_name"] = baseRepo.Name
 	c.Data["description"] = baseRepo.Description
 	c.Data["IsPrivate"] = baseRepo.IsPrivate
+	c.Data["IsUnlisted"] = baseRepo.IsUnlisted
 
 	if err = baseRepo.GetOwner(); err != nil {
 		c.Error(err, "get owner")

+ 2 - 0
internal/route/repo/repo.go

@@ -127,6 +127,7 @@ func CreatePost(c *context.Context, f form.CreateRepo) {
 		License:     f.License,
 		Readme:      f.Readme,
 		IsPrivate:   f.Private || conf.Repository.ForcePrivate,
+		IsUnlisted:  f.Unlisted,
 		AutoInit:    f.AutoInit,
 	})
 	if err == nil {
@@ -198,6 +199,7 @@ func MigratePost(c *context.Context, f form.MigrateRepo) {
 		Name:        f.RepoName,
 		Description: f.Description,
 		IsPrivate:   f.Private || conf.Repository.ForcePrivate,
+		IsUnlisted:  f.Unlisted,
 		IsMirror:    f.Mirror,
 		RemoteAddr:  remoteAddr,
 	})

+ 3 - 2
internal/route/repo/setting.go

@@ -88,11 +88,12 @@ func SettingsPost(c *context.Context, f form.RepoSetting) {
 		// Visibility of forked repository is forced sync with base repository.
 		if repo.IsFork {
 			f.Private = repo.BaseRepo.IsPrivate
+			f.Unlisted = repo.BaseRepo.IsUnlisted
 		}
 
-		visibilityChanged := repo.IsPrivate != f.Private
+		visibilityChanged := repo.IsPrivate != f.Private || repo.IsUnlisted != f.Unlisted
 		repo.IsPrivate = f.Private
-		repo.Unlisted = !f.Listed
+		repo.IsUnlisted = f.Unlisted
 		if err := db.UpdateRepository(repo, visibilityChanged); err != nil {
 			c.Error(err, "update repository")
 			return

+ 1 - 1
internal/route/routes_gin.go

@@ -6,7 +6,7 @@ func filterUnlistedRepos(repos []*db.Repository) []*db.Repository {
 	// Filter out Unlisted repositories
 	var showRep []*db.Repository
 	for _, repo := range repos {
-		if !repo.Unlisted {
+		if !repo.IsUnlisted {
 			showRep = append(showRep, repo)
 		}
 	}

+ 2 - 11
internal/route/search_gin.go

@@ -22,22 +22,13 @@ const (
 type set map[int64]interface{}
 
 func newset() set {
-	return make(map[int64]interface{}, 0)
+	return make(map[int64]interface{})
 }
 
 func (s set) add(item int64) {
 	s[item] = nil
 }
 
-func (s set) contains(item int64) bool {
-	_, yes := s[item]
-	return yes
-}
-
-func (s set) remove(item int64) {
-	delete(s, item)
-}
-
 func (s set) asSlice() []int64 {
 	slice := make([]int64, len(s))
 	idx := 0
@@ -82,7 +73,7 @@ func collectSearchableRepoIDs(c *context.Context) ([]int64, error) {
 	// If it's not unlisted, add it to the set
 	// This will add public (listed) repositories
 	for _, r := range repos {
-		if !r.Unlisted {
+		if !r.IsUnlisted {
 			repoIDSet.add(r.ID)
 		}
 	}

+ 1 - 1
internal/route/suggest.go

@@ -32,5 +32,5 @@ func ExploreSuggest(c *context.Context) {
 		return
 	}
 
-	c.Write(data)
+	_, _ = c.Write(data)
 }

+ 1 - 1
public/css/custom.css

@@ -368,7 +368,7 @@ textarea#description {
   display: none !important;
 }
 .footericon {
-  height: 35px;
+  height: 30px;
   vertical-align: middle;
 }
 .footertext {

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/css/gogs.min.css.map


Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
public/img/elife-logo-xs.fd623d00.svg


BIN
public/img/lmu.png


BIN
public/img/re3data_logo.png


+ 1 - 0
templates/admin/repo/list.tmpl

@@ -35,6 +35,7 @@
 									<td><a href="{{AppSubURL}}/{{.Owner.Name}}/{{.Name}}">{{.Name}}</a></td>
 									<td><i class="fa fa{{if .IsPrivate}}-check{{end}}-square-o"></i></td>
 									<td>{{.NumWatches}}</td>
+									<td>{{.NumStars}}</td>
 									<td>{{.NumIssues}}</td>
 									<td>{{.Size | FileSize}}</td>
 									<td><span title="{{DateFmtLong .Created}}">{{DateFmtShort .Created}}</span></td>

+ 6 - 2
templates/base/footer.tmpl

@@ -28,8 +28,12 @@
 				<span>Powered by:      <a href="https://github.com/gogs/gogs"><img class="ui mini footericon" src="{{AppSubURL}}/img/gogs.svg"/></a>         </span>
 				<span>Hosted by:       <a href="http://neuro.bio.lmu.de"><img class="ui mini footericon" src="{{AppSubURL}}/img/lmu.png"/></a>          </span>
 				<span>Funded by:       <a href="http://www.bmbf.de"><img class="ui mini footericon" src="{{AppSubURL}}/img/bmbf.png"/></a>         </span>
-				<span>Registered with: <a href="http://doi.org/10.17616/R3SX9N"><img class="ui mini footericon" src="{{AppSubURL}}/img/re3.png"/></a>          </span>
-				<span>Recommended by:  <a href="https://www.nature.com/sdata/policies/repositories#neurosci"><img class="ui mini footericon" src="{{AppSubURL}}/img/sdatarecbadge.jpg"/><a href="https://journals.plos.org/plosone/s/data-availability#loc-neuroscience"><img class="ui mini footericon" src="{{AppSubURL}}/img/sm_plos-logo-sm.png"/></a></span>
+				<span>Registered with: <a href="http://doi.org/10.17616/R3SX9N"><img class="ui mini footericon" src="{{AppSubURL}}/img/re3data_logo.png"/></a>          </span>
+				<span>Recommended by:  
+					<a href="https://www.nature.com/sdata/policies/repositories#neurosci"><img class="ui mini footericon" src="{{AppSubURL}}/img/sdatarecbadge.jpg"/></a>
+					<a href="https://fairsharing.org/recommendation/PLOS"><img class="ui mini footericon" src="{{AppSubURL}}/img/sm_plos-logo-sm.png"/></a>
+					<a href="https://fairsharing.org/recommendation/eLifeRecommendedRepositoriesandStandards"><img class="ui mini footericon" src="{{AppSubURL}}/img/elife-logo-xs.fd623d00.svg"/></a>
+				</span>
 			</div>
 		</div>
 	</footer>

+ 2 - 0
templates/explore/repo_list.tmpl

@@ -10,6 +10,8 @@
 						<a class="name" href="{{AppSubURL}}/{{if .Owner}}{{.Owner.Name}}{{else if $.Org}}{{$.Org.Name}}{{else}}{{$.Owner.Name}}{{end}}/{{.Name}}">{{if $.PageIsExplore}}{{.Owner.Name}} / {{end}}{{.Name}}</a>
 						{{if .IsPrivate}}
 							<span class="text gold"><i class="octicon octicon-lock"></i></span>
+						{{else if .IsUnlisted}}
+							<span><i class="octicon octicon-eye"></i></span>
 						{{else if .IsFork}}
 							<span><i class="octicon octicon-repo-forked"></i></span>
 						{{else if .IsMirror}}

+ 2 - 0
templates/mail/issue/comment.tmpl

@@ -10,6 +10,8 @@
 	<p>
 		---
 		<br>
+		<i>Please do not reply to this email. This mailbox is not monitored. Click the link below to comment on the issue.</i>
+		<br>
 		<a href="{{.Link}}">View it on GIN</a>.
 	</p>
 </body>

+ 2 - 0
templates/mail/issue/mention.tmpl

@@ -11,6 +11,8 @@
 	<p>
 		---
 		<br>
+		<i>Please do not reply to this email. This mailbox is not monitored. Click the link below to comment on the issue.</i>
+		<br>
 		<a href="{{.Link}}">View it on GIN</a>.
 	</p>
 </body>

+ 3 - 1
templates/mail/notify/collaborator.tmpl

@@ -10,7 +10,9 @@
 	<p>
 		---
 		<br>
-		<a href="{{.Link}}">View it on Gin</a>.
+		<i>Please do not reply to this email. This mailbox is not monitored. Click the link below to comment on the issue.</i>
+		<br>
+		<a href="{{.Link}}">View it on GIN</a>.
 	</p>
 </body>
 </html>

+ 7 - 0
templates/repo/create.tmpl

@@ -50,6 +50,13 @@
 							{{end}}
 						</div>
 					</div>
+					<div class="inline field">
+						<label></label>
+						<div class="ui checkbox">
+							<input name="unlisted" type="checkbox">
+							<label>{{.i18n.Tr "repo.unlisted_helper" | Safe}}</label>
+						</div>
+					</div>
 					<div class="inline field {{if .Err_Description}}error{{end}}">
 						<label for="description">{{.i18n.Tr "repo.repo_desc"}}</label>
 						<textarea class="autosize" id="description" name="description" rows="3">{{.description}}</textarea>

+ 2 - 2
templates/repo/header.tmpl

@@ -7,9 +7,9 @@
 					<div class="ui huge breadcrumb">
 						{{if .UseCustomAvatar}}
 							<img class="ui mini spaced image" src="{{.RelAvatarLink}}">
-							<i class="{{if .IsPrivate}}mega-octicon octicon-lock{{else if .IsMirror}}mega-octicon octicon-repo-clone{{else if .IsFork}}mega-octicon octicon-repo-forked{{end}}"></i>
+							<i class="{{if .IsPrivate}}mega-octicon octicon-lock{{else if .IsUnlisted}}mega-octicon octicon-eye{{else if .IsMirror}}mega-octicon octicon-repo-clone{{else if .IsFork}}mega-octicon octicon-repo-forked{{end}}"></i>
 						{{else}}
-							<i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i>
+							<i class="mega-octicon octicon-{{if .IsPrivate}}lock{{else if .IsUnlisted}}eye{{else if .IsMirror}}repo-clone{{else if .IsFork}}repo-forked{{else}}repo{{end}}"></i>
 						{{end}}
 						<a href="{{AppSubURL}}/{{.Owner.Name}}">{{.Owner.Name}}</a>
 						<div class="divider"> / </div>

+ 7 - 0
templates/repo/migrate.tmpl

@@ -80,6 +80,13 @@
 							{{end}}
 						</div>
 					</div>
+					<div class="inline field">
+						<label></label>
+						<div class="ui checkbox">
+							<input name="unlisted" type="checkbox">
+							<label>{{.i18n.Tr "repo.unlisted_helper" | Safe}}</label>
+						</div>
+					</div>
 					<div class="inline field">
 						<label>{{.i18n.Tr "repo.migrate_type"}}</label>
 						<div class="ui checkbox">

+ 7 - 0
templates/repo/pulls/fork.tmpl

@@ -49,6 +49,13 @@
 							<input type="checkbox" {{if .IsPrivate}}checked{{end}}>
 							<label>{{.i18n.Tr "repo.visiblity_helper" | Safe}}</label>
 						</div>
+					</div>
+					<div class="inline field">
+						<label></label>
+						<div class="ui read-only checkbox">
+							<input type="checkbox" {{if .IsUnlisted}}checked{{end}}>
+							<label>{{.i18n.Tr "repo.unlisted_helper" | Safe}}</label>
+						</div>
 						<span class="help">{{.i18n.Tr "repo.fork_visiblity_helper"}}</span>
 					</div>
 					<div class="inline field {{if .Err_Description}}error{{end}}">

+ 13 - 10
templates/repo/settings/options.tmpl

@@ -31,18 +31,21 @@
 						{{if not .Repository.IsFork}}
 							<div class="inline field">
 								<label>{{.i18n.Tr "repo.visibility"}}</label>
-								<div class="ui checkbox" {{if .Repository.IsPrivate}}data-tooltip="{{.i18n.Tr "repo.license_warn"}}"{{end}}>
-									<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}>
-									<label>{{.i18n.Tr "repo.visiblity_helper" | Safe}} {{if .Repository.NumForks}}<span class="text red">{{.i18n.Tr "repo.visiblity_fork_helper"}}</span>{{end}}</label>
+								<div class="ui segment">
+									<div class="field">
+										<div class="ui checkbox" {{if .Repository.IsPrivate}}data-tooltip="{{.i18n.Tr "repo.license_warn"}}"{{end}}>
+											<input name="private" type="checkbox" {{if .Repository.IsPrivate}}checked{{end}}>
+											<label>{{.i18n.Tr "repo.visiblity_helper" | Safe}} {{if .Repository.NumForks}}<span class="text red">{{.i18n.Tr "repo.visiblity_fork_helper"}}</span>{{end}}</label>
+										</div>
+									</div>
+									<div class="field">
+										<div class="ui checkbox">
+											<input name="unlisted" type="checkbox" {{if .Repository.IsUnlisted}}checked{{end}}>
+											<label>{{.i18n.Tr "repo.unlisted_helper" | Safe}} {{if .Repository.NumForks}}<span class="text red">{{.i18n.Tr "repo.visiblity_fork_helper"}}</span>{{end}}</label>
+										</div>
+									</div>
 								</div>
 							</div>
-						<div class="inline field">
-							<label>{{.i18n.Tr "repo.listed"}}</label>
-							<div class="ui checkbox">
-								<input name="listed" type="checkbox" {{if not .Repository.Unlisted}}checked{{end}}>
-								<label>{{.i18n.Tr "repo.listed_helper" | Safe}}</label>
-							</div>
-						</div>
 						{{end}}
 
 						<div class="field">

Неке датотеке нису приказане због велике количине промена