Parcourir la source

git: migrate to github.com/gogs/git-module@v1.0.0 (#5958)

* WIP

* Finish `internal/db/git_diff.go`

* FInish internal/db/mirror.go

* Finish internal/db/pull.go

* Finish internal/db/release.go

* Finish internal/db/repo.go

* Finish internal/db/repo_branch.go

* Finish internal/db/repo_editor.go

* Finish internal/db/update.go

* Save my work

* Add license header

* Compile!

* Merge master

* Finish internal/cmd/hook.go

* Finish internal/conf/static.go

* Finish internal/context/repo.go

* Finish internal/db/action.go

* Finish internal/db/git_diff.go

* Fix submodule URL inferring

* Finish internal/db/mirror.go

* Updat to beta.4

* css: update fonts

* Finish internal/db/pull.go

* Finish internal/db/release.go

* Finish internal/db/repo_branch.go

* Finish internal/db/wiki.go

* gitutil: enhance infer submodule UR

* Finish internal/route/api/v1/repo/commits.go

* mirror: only collect branch commits after sync

* mirror: fix tag support

* Finish internal/db/repo.go

* Finish internal/db/repo_editor.go

* Finish internal/db/update.go

* Finish internal/gitutil/pull_request.go

* Make it compile

* Finish internal/route/repo/setting.go

* Finish internal/route/repo/branch.go

* Finish internal/route/api/v1/repo/file.go

* Finish internal/route/repo/download.go

* Finish internal/route/repo/editor.go

* Use helper

* Finish internal/route/repo/issue.go

* Finish internal/route/repo/pull.go

* Finish internal/route/repo/release.go

* Finish internal/route/repo/repo.go

* Finish internal/route/repo/wiki.go

* Finish internal/route/repo/commit.go

* Finish internal/route/repo/view.go

* Finish internal/gitutil/tag.go

* go.sum
ᴜɴᴋɴᴡᴏɴ il y a 4 ans
Parent
commit
6437d0180b
71 fichiers modifiés avec 3168 ajouts et 2680 suppressions
  1. 3 3
      conf/app.ini
  2. 4 3
      go.mod
  3. 12 0
      go.sum
  4. 1 1
      internal/assets/conf/conf_gen.go
  5. 4 4
      internal/assets/public/public_gen.go
  6. 9 9
      internal/assets/templates/templates_gen.go
  7. 11 10
      internal/cmd/hook.go
  8. 6 6
      internal/conf/static.go
  9. 1 1
      internal/context/context.go
  10. 43 45
      internal/context/repo.go
  11. 14 14
      internal/db/action.go
  12. 0 194
      internal/db/git_diff.go
  13. 0 35
      internal/db/git_diff_test.go
  14. 5 5
      internal/db/migrations/v16.go
  15. 52 103
      internal/db/mirror.go
  16. 18 89
      internal/db/mirror_test.go
  17. 58 43
      internal/db/pull.go
  18. 6 6
      internal/db/release.go
  19. 61 56
      internal/db/repo.go
  20. 14 14
      internal/db/repo_branch.go
  21. 60 66
      internal/db/repo_editor.go
  22. 23 28
      internal/db/update.go
  23. 11 18
      internal/db/user.go
  24. 3 3
      internal/db/webhook_dingtalk.go
  25. 3 3
      internal/db/webhook_discord.go
  26. 3 3
      internal/db/webhook_slack.go
  27. 14 20
      internal/db/wiki.go
  28. 196 0
      internal/gitutil/diff.go
  29. 49 0
      internal/gitutil/diff_test.go
  30. 19 0
      internal/gitutil/error.go
  31. 23 0
      internal/gitutil/error_test.go
  32. 23 0
      internal/gitutil/mock.go
  33. 90 0
      internal/gitutil/module.go
  34. 69 0
      internal/gitutil/pull_request.go
  35. 108 0
      internal/gitutil/pull_request_test.go
  36. 48 0
      internal/gitutil/submodule.go
  37. 58 0
      internal/gitutil/submodule_test.go
  38. 95 0
      internal/gitutil/tag.go
  39. 109 0
      internal/gitutil/tag_test.go
  40. 1 1
      internal/route/api/v1/convert/convert.go
  41. 19 18
      internal/route/api/v1/repo/commits.go
  42. 30 45
      internal/route/api/v1/repo/contents.go
  43. 8 7
      internal/route/api/v1/repo/file.go
  44. 13 8
      internal/route/api/v1/repo/tree.go
  45. 2 2
      internal/route/install.go
  46. 2 2
      internal/route/repo/branch.go
  47. 36 47
      internal/route/repo/commit.go
  48. 16 25
      internal/route/repo/download.go
  49. 23 30
      internal/route/repo/editor.go
  50. 5 14
      internal/route/repo/issue.go
  51. 72 68
      internal/route/repo/pull.go
  52. 24 22
      internal/route/repo/release.go
  53. 19 18
      internal/route/repo/repo.go
  54. 14 17
      internal/route/repo/setting.go
  55. 40 49
      internal/route/repo/view.go
  56. 45 31
      internal/route/repo/webhook.go
  57. 31 36
      internal/route/repo/wiki.go
  58. 20 27
      internal/template/template.go
  59. 56 62
      public/css/gogs.css
  60. 0 0
      public/css/gogs.css.map
  61. 377 371
      public/less/_base.less
  62. 948 949
      public/less/_repository.less
  63. 3 3
      templates/admin/config.tmpl
  64. 2 2
      templates/base/head.tmpl
  65. 2 3
      templates/repo/commits_table.tmpl
  66. 17 17
      templates/repo/diff/box.tmpl
  67. 5 5
      templates/repo/diff/section_unified.tmpl
  68. 1 1
      templates/repo/settings/githook_edit.tmpl
  69. 1 1
      templates/repo/settings/githooks.tmpl
  70. 1 1
      templates/repo/settings/options.tmpl
  71. 9 16
      templates/repo/view_list.tmpl

+ 3 - 3
conf/app.ini

@@ -424,12 +424,12 @@ OLDER_THAN = 24h
 [git]
 ; Disables highlight of added and removed changes
 DISABLE_DIFF_HIGHLIGHT = false
+; Max number of files shown in diff view
+MAX_GIT_DIFF_FILES = 100
 ; Max number of lines allowed of a single file in diff view
 MAX_GIT_DIFF_LINES = 1000
 ; Max number of characters of a line allowed in diff view
-MAX_GIT_DIFF_LINE_CHARACTERS = 500
-; Max number of files shown in diff view
-MAX_GIT_DIFF_FILES = 100
+MAX_GIT_DIFF_LINE_CHARACTERS = 2000
 ; Arguments for command 'git gc', e.g. "--aggressive --auto"
 ; see more on http://git-scm.com/docs/git-gc/1.7.5
 GC_ARGS =

+ 4 - 3
go.mod

@@ -5,7 +5,7 @@ go 1.12
 require (
 	github.com/bgentry/speakeasy v0.1.0 // indirect
 	github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0
-	github.com/editorconfig/editorconfig-core-go/v2 v2.2.1
+	github.com/editorconfig/editorconfig-core-go/v2 v2.3.0
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/go-macaron/binding v1.0.1
 	github.com/go-macaron/cache v0.0.0-20190810181446-10f7c57e2196
@@ -18,10 +18,11 @@ require (
 	github.com/go-sql-driver/mysql v1.4.1
 	github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561
 	github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14
-	github.com/gogs/git-module v0.8.3
+	github.com/gogs/git-module v1.0.0
 	github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4
 	github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0
 	github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a
+	github.com/google/go-cmp v0.3.0
 	github.com/google/go-github v17.0.0+incompatible
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/issue9/identicon v1.0.1
@@ -61,7 +62,7 @@ require (
 	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 	gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
 	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
-	gopkg.in/ini.v1 v1.52.0
+	gopkg.in/ini.v1 v1.53.0
 	gopkg.in/ldap.v2 v2.5.1
 	gopkg.in/macaron.v1 v1.3.4
 	unknwon.dev/clog/v2 v2.1.2

+ 12 - 0
go.sum

@@ -41,6 +41,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/editorconfig/editorconfig-core-go/v2 v2.2.1 h1:jY5PCRQf4V0oqpim/Ympl6MwHcb9+nBHEnHOPXqNZ/A=
 github.com/editorconfig/editorconfig-core-go/v2 v2.2.1/go.mod h1:6XDmqAZsQu8ikS+onLRJfLZvTP3RWTVT8ROX6qcdkio=
+github.com/editorconfig/editorconfig-core-go/v2 v2.3.0 h1:QD1YB/rbntMEQIKM42kQOaqGdS13UvGsl9c8m/nFNWY=
+github.com/editorconfig/editorconfig-core-go/v2 v2.3.0/go.mod h1:RNdPfKd9PliYEUZ3r+GxbDsSHNnEluC1wdkQJc3jD4k=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
 github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@@ -82,6 +84,10 @@ github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14 h1:yXtpJr/LV6PFu4nTLgfjQ
 github.com/gogs/cron v0.0.0-20171120032916-9f6c956d3e14/go.mod h1:jPoNZLWDAqA5N3G5amEoiNbhVrmM+ZQEcnQvNQ2KaZk=
 github.com/gogs/git-module v0.8.3 h1:9f8oxSs9OACWrGBYMVnnQNzyTcVN+zzcBM7CXnbmezw=
 github.com/gogs/git-module v0.8.3/go.mod h1:aj4tcm7DxaszJWpZLZIRL6gfPXyguAHiE1PDfAAPrCw=
+github.com/gogs/git-module v1.0.0-beta.4 h1:5CyCvTfrb2n5LRpHcNIaFnywHDkM/NxSZVP6t4tpTXI=
+github.com/gogs/git-module v1.0.0-beta.4/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
+github.com/gogs/git-module v1.0.0 h1:iOlCZ5kPc3RjnWRxdziL5hjCaosYyZw/Lf2odzR/kjw=
+github.com/gogs/git-module v1.0.0/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
 github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4 h1:C7NryI/RQhsIWwC2bHN601P1wJKeuQ6U/UCOYTn3Cic=
 github.com/gogs/go-gogs-client v0.0.0-20200128182646-c69cb7680fd4/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU=
 github.com/gogs/go-libravatar v0.0.0-20191106065024-33a75213d0a0 h1:K02vod+sn3M1OOkdqi2tPxN2+xESK4qyITVQ3JkGEv4=
@@ -305,6 +311,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -363,8 +370,11 @@ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AW
 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.46.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4=
 gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.53.0 h1:c7ruDvTQi0MUTFuNpDRXLSjs7xT4TerM1icIg4uKWRg=
+gopkg.in/ini.v1 v1.53.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
 gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
 gopkg.in/macaron.v1 v1.3.4 h1:HvIscOwxhFhx3swWM/979wh2QMYyuXrNmrF9l+j3HZs=
@@ -375,6 +385,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
+gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 1
internal/assets/conf/conf_gen.go


Fichier diff supprimé car celui-ci est trop grand
+ 4 - 4
internal/assets/public/public_gen.go


Fichier diff supprimé car celui-ci est trop grand
+ 9 - 9
internal/assets/templates/templates_gen.go


+ 11 - 10
internal/cmd/hook.go

@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"crypto/tls"
 	"fmt"
+	"net/url"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -25,7 +26,6 @@ import (
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/email"
 	"gogs.io/gogs/internal/httplib"
-	"gogs.io/gogs/internal/template"
 )
 
 var (
@@ -87,7 +87,7 @@ func runHookPreReceive(c *cli.Context) error {
 		}
 		oldCommitID := string(fields[0])
 		newCommitID := string(fields[1])
-		branchName := strings.TrimPrefix(string(fields[2]), git.BRANCH_PREFIX)
+		branchName := git.RefShortName(string(fields[2]))
 
 		// Branch protection
 		repoID := com.StrTo(os.Getenv(db.ENV_REPO_ID)).MustInt64()
@@ -121,7 +121,7 @@ func runHookPreReceive(c *cli.Context) error {
 		}
 
 		// check and deletion
-		if newCommitID == git.EMPTY_SHA {
+		if newCommitID == git.EmptyID {
 			fail(fmt.Sprintf("Branch '%s' is protected from deletion", branchName), "")
 		}
 
@@ -221,7 +221,7 @@ func runHookPostReceive(c *cli.Context) error {
 		options := db.PushUpdateOptions{
 			OldCommitID:  string(fields[0]),
 			NewCommitID:  string(fields[1]),
-			RefFullName:  string(fields[2]),
+			FullRefspec:  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),
@@ -232,19 +232,20 @@ func runHookPostReceive(c *cli.Context) error {
 		}
 
 		// Ask for running deliver hook and test pull request tasks
-		reqURL := conf.Server.LocalRootURL + 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)
+		q := make(url.Values)
+		q.Add("branch", git.RefShortName(options.FullRefspec))
+		q.Add("secret", os.Getenv(db.ENV_REPO_OWNER_SALT_MD5))
+		q.Add("pusher", os.Getenv(db.ENV_AUTH_USER_ID))
+		reqURL := fmt.Sprintf("%s%s/%s/tasks/trigger?%s", conf.Server.LocalRootURL, options.RepoUserName, options.RepoName, q.Encode())
 		log.Trace("Trigger task: %s", reqURL)
 
 		resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
 			InsecureSkipVerify: true,
 		}).Response()
 		if err == nil {
-			resp.Body.Close()
+			_ = resp.Body.Close()
 			if resp.StatusCode/100 != 2 {
-				log.Error("Failed to trigger task: not 2xx response code")
+				log.Error("Failed to trigger task: unsuccessful response code %d", resp.StatusCode)
 			}
 		} else {
 			log.Error("Failed to trigger task: %v", err)

+ 6 - 6
internal/conf/static.go

@@ -351,12 +351,12 @@ var (
 		// ⚠️ WARNING: Should only be set by "internal/db/repo.go".
 		Version string `ini:"-"`
 
-		DisableDiffHighlight     bool
-		MaxGitDiffLines          int
-		MaxGitDiffLineCharacters int
-		MaxGitDiffFiles          int
-		GCArgs                   []string `ini:"GC_ARGS" delim:" "`
-		Timeout                  struct {
+		DisableDiffHighlight bool
+		MaxDiffFiles         int      `ini:"MAX_GIT_DIFF_FILES"`
+		MaxDiffLines         int      `ini:"MAX_GIT_DIFF_LINES"`
+		MaxDiffLineChars     int      `ini:"MAX_GIT_DIFF_LINE_CHARACTERS"`
+		GCArgs               []string `ini:"GC_ARGS" delim:" "`
+		Timeout              struct {
 			Migrate int
 			Mirror  int
 			Clone   int

+ 1 - 1
internal/context/context.go

@@ -173,7 +173,7 @@ func (c *Context) Handle(status int, msg string, err error) {
 		c.Data["Title"] = "Page Not Found"
 	case http.StatusInternalServerError:
 		c.Data["Title"] = "Internal Server Error"
-		log.Error("%s: %v", msg, err)
+		log.ErrorDepth(5, "%s: %v", msg, err)
 		if !conf.IsProdMode() || (c.IsLogged && c.User.IsAdmin) {
 			c.Data["ErrorMsg"] = err
 		}

+ 43 - 45
internal/context/repo.go

@@ -5,19 +5,20 @@
 package context
 
 import (
+	"bytes"
 	"fmt"
-	"io/ioutil"
 	"net/url"
 	"strings"
 
 	"github.com/editorconfig/editorconfig-core-go/v2"
+	"github.com/pkg/errors"
 	"gopkg.in/macaron.v1"
 
 	"github.com/gogs/git-module"
 
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db"
-	"gogs.io/gogs/internal/db/errors"
+	dberrors "gogs.io/gogs/internal/db/errors"
 )
 
 type PullRequest struct {
@@ -75,26 +76,23 @@ func (r *Repository) CanEnableEditor() bool {
 	return r.Repository.CanEnableEditor() && r.IsViewBranch && r.IsWriter() && !r.Repository.IsBranchRequirePullRequest(r.BranchName)
 }
 
-// GetEditorconfig returns the .editorconfig definition if found in the
-// HEAD of the default repo branch.
-func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
-	commit, err := r.GitRepo.GetBranchCommit(r.Repository.DefaultBranch)
+// Editorconfig returns the ".editorconfig" definition if found in the HEAD of the default branch.
+func (r *Repository) Editorconfig() (*editorconfig.Editorconfig, error) {
+	commit, err := r.GitRepo.BranchCommit(r.Repository.DefaultBranch)
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrapf(err, "get commit of branch %q ", r.Repository.DefaultBranch)
 	}
-	treeEntry, err := commit.GetTreeEntryByPath(".editorconfig")
-	if err != nil {
-		return nil, err
-	}
-	reader, err := treeEntry.Blob().Data()
+
+	entry, err := commit.TreeEntry(".editorconfig")
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "get .editorconfig")
 	}
-	data, err := ioutil.ReadAll(reader)
+
+	p, err := entry.Blob().Bytes()
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "read .editorconfig")
 	}
-	return editorconfig.ParseBytes(data)
+	return editorconfig.Parse(bytes.NewReader(p))
 }
 
 // MakeURL accepts a string or url.URL as argument and returns escaped URL prepended with repository URL.
@@ -149,7 +147,7 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 		} else {
 			owner, err = db.GetUserByName(ownerName)
 			if err != nil {
-				c.NotFoundOrServerError("GetUserByName", errors.IsUserNotExist, err)
+				c.NotFoundOrServerError("GetUserByName", dberrors.IsUserNotExist, err)
 				return
 			}
 		}
@@ -158,7 +156,7 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 
 		repo, err := db.GetRepositoryByName(owner.ID, repoName)
 		if err != nil {
-			c.NotFoundOrServerError("GetRepositoryByName", errors.IsRepoNotExist, err)
+			c.NotFoundOrServerError("GetRepositoryByName", dberrors.IsRepoNotExist, err)
 			return
 		}
 
@@ -222,16 +220,16 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 			c.Data["Mirror"] = c.Repo.Mirror
 		}
 
-		gitRepo, err := git.OpenRepository(db.RepoPath(ownerName, repoName))
+		gitRepo, err := git.Open(db.RepoPath(ownerName, repoName))
 		if err != nil {
-			c.ServerError(fmt.Sprintf("RepoAssignment Invalid repo '%s'", c.Repo.Repository.RepoPath()), err)
+			c.ServerError("open repository", err)
 			return
 		}
 		c.Repo.GitRepo = gitRepo
 
-		tags, err := c.Repo.GitRepo.GetTags()
+		tags, err := c.Repo.GitRepo.Tags()
 		if err != nil {
-			c.ServerError(fmt.Sprintf("GetTags '%s'", c.Repo.Repository.RepoPath()), err)
+			c.ServerError("get tags", err)
 			return
 		}
 		c.Data["Tags"] = tags
@@ -260,21 +258,21 @@ func RepoAssignment(pages ...bool) macaron.Handler {
 		}
 
 		c.Data["TagName"] = c.Repo.TagName
-		brs, err := c.Repo.GitRepo.GetBranches()
+		branches, err := c.Repo.GitRepo.Branches()
 		if err != nil {
-			c.ServerError("GetBranches", err)
+			c.ServerError("get branches", err)
 			return
 		}
-		c.Data["Branches"] = brs
-		c.Data["BrancheCount"] = len(brs)
+		c.Data["Branches"] = branches
+		c.Data["BrancheCount"] = len(branches)
 
 		// 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.IsBranchExist(c.Repo.Repository.DefaultBranch) {
+			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]
+			} else if len(branches) > 0 {
+				c.Repo.BranchName = branches[0]
 			}
 		}
 		c.Data["BranchName"] = c.Repo.BranchName
@@ -300,7 +298,7 @@ func RepoRef() macaron.Handler {
 		// For API calls.
 		if c.Repo.GitRepo == nil {
 			repoPath := db.RepoPath(c.Repo.Owner.Name, c.Repo.Repository.Name)
-			c.Repo.GitRepo, err = git.OpenRepository(repoPath)
+			c.Repo.GitRepo, err = git.Open(repoPath)
 			if err != nil {
 				c.Handle(500, "RepoRef Invalid repo "+repoPath, err)
 				return
@@ -310,17 +308,17 @@ func RepoRef() macaron.Handler {
 		// Get default branch.
 		if len(c.Params("*")) == 0 {
 			refName = c.Repo.Repository.DefaultBranch
-			if !c.Repo.GitRepo.IsBranchExist(refName) {
-				brs, err := c.Repo.GitRepo.GetBranches()
+			if !c.Repo.GitRepo.HasBranch(refName) {
+				branches, err := c.Repo.GitRepo.Branches()
 				if err != nil {
-					c.Handle(500, "GetBranches", err)
+					c.ServerError("get branches", err)
 					return
 				}
-				refName = brs[0]
+				refName = branches[0]
 			}
-			c.Repo.Commit, err = c.Repo.GitRepo.GetBranchCommit(refName)
+			c.Repo.Commit, err = c.Repo.GitRepo.BranchCommit(refName)
 			if err != nil {
-				c.Handle(500, "GetBranchCommit", err)
+				c.ServerError("get branch commit", err)
 				return
 			}
 			c.Repo.CommitID = c.Repo.Commit.ID.String()
@@ -332,8 +330,8 @@ func RepoRef() macaron.Handler {
 			for i, part := range parts {
 				refName = strings.TrimPrefix(refName+"/"+part, "/")
 
-				if c.Repo.GitRepo.IsBranchExist(refName) ||
-					c.Repo.GitRepo.IsTagExist(refName) {
+				if c.Repo.GitRepo.HasBranch(refName) ||
+					c.Repo.GitRepo.HasTag(refName) {
 					if i < len(parts)-1 {
 						c.Repo.TreePath = strings.Join(parts[i+1:], "/")
 					}
@@ -346,21 +344,21 @@ func RepoRef() macaron.Handler {
 				c.Repo.TreePath = strings.Join(parts[1:], "/")
 			}
 
-			if c.Repo.GitRepo.IsBranchExist(refName) {
+			if c.Repo.GitRepo.HasBranch(refName) {
 				c.Repo.IsViewBranch = true
 
-				c.Repo.Commit, err = c.Repo.GitRepo.GetBranchCommit(refName)
+				c.Repo.Commit, err = c.Repo.GitRepo.BranchCommit(refName)
 				if err != nil {
-					c.Handle(500, "GetBranchCommit", err)
+					c.ServerError("get branch commit", err)
 					return
 				}
 				c.Repo.CommitID = c.Repo.Commit.ID.String()
 
-			} else if c.Repo.GitRepo.IsTagExist(refName) {
+			} else if c.Repo.GitRepo.HasTag(refName) {
 				c.Repo.IsViewTag = true
-				c.Repo.Commit, err = c.Repo.GitRepo.GetTagCommit(refName)
+				c.Repo.Commit, err = c.Repo.GitRepo.TagCommit(refName)
 				if err != nil {
-					c.Handle(500, "GetTagCommit", err)
+					c.ServerError("get tag commit", err)
 					return
 				}
 				c.Repo.CommitID = c.Repo.Commit.ID.String()
@@ -368,7 +366,7 @@ func RepoRef() macaron.Handler {
 				c.Repo.IsViewCommit = true
 				c.Repo.CommitID = refName
 
-				c.Repo.Commit, err = c.Repo.GitRepo.GetCommit(refName)
+				c.Repo.Commit, err = c.Repo.GitRepo.CatFileCommit(refName)
 				if err != nil {
 					c.NotFound()
 					return

+ 14 - 14
internal/db/action.go

@@ -268,9 +268,9 @@ func (pc *PushCommits) ToApiPayloadCommits(repoPath, repoURL string) ([]*api.Pay
 			return nil, fmt.Errorf("GetUserByEmail: %v", err)
 		}
 
-		fileStatus, err := git.GetCommitFileStatus(repoPath, commit.Sha1)
+		nameStatus, err := git.RepoShowNameStatus(repoPath, commit.Sha1)
 		if err != nil {
-			return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %v", commit.Sha1, err)
+			return nil, fmt.Errorf("show name status [commit_sha1: %s]: %v", commit.Sha1, err)
 		}
 
 		commits[i] = &api.PayloadCommit{
@@ -287,9 +287,9 @@ func (pc *PushCommits) ToApiPayloadCommits(repoPath, repoURL string) ([]*api.Pay
 				Email:    commit.CommitterEmail,
 				UserName: committerUsername,
 			},
-			Added:     fileStatus.Added,
-			Removed:   fileStatus.Removed,
-			Modified:  fileStatus.Modified,
+			Added:     nameStatus.Added,
+			Removed:   nameStatus.Removed,
+			Modified:  nameStatus.Modified,
 			Timestamp: commit.Timestamp,
 		}
 	}
@@ -298,21 +298,21 @@ func (pc *PushCommits) ToApiPayloadCommits(repoPath, repoURL string) ([]*api.Pay
 
 // AvatarLink tries to match user in database with e-mail
 // in order to show custom avatar, and falls back to general avatar link.
-func (push *PushCommits) AvatarLink(email string) string {
-	_, ok := push.avatars[email]
+func (pcs *PushCommits) AvatarLink(email string) string {
+	_, ok := pcs.avatars[email]
 	if !ok {
 		u, err := GetUserByEmail(email)
 		if err != nil {
-			push.avatars[email] = tool.AvatarLink(email)
+			pcs.avatars[email] = tool.AvatarLink(email)
 			if !errors.IsUserNotExist(err) {
 				log.Error("GetUserByEmail: %v", err)
 			}
 		} else {
-			push.avatars[email] = u.RelAvatarLink()
+			pcs.avatars[email] = u.RelAvatarLink()
 		}
 	}
 
-	return push.avatars[email]
+	return pcs.avatars[email]
 }
 
 // UpdateIssuesCommit checks if issues are manipulated by commit message.
@@ -474,12 +474,12 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 		return fmt.Errorf("UpdateRepository: %v", err)
 	}
 
-	isNewRef := opts.OldCommitID == git.EMPTY_SHA
-	isDelRef := opts.NewCommitID == git.EMPTY_SHA
+	isNewRef := opts.OldCommitID == git.EmptyID
+	isDelRef := opts.NewCommitID == git.EmptyID
 
 	opType := ACTION_COMMIT_REPO
 	// Check if it's tag push or branch.
-	if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
+	if strings.HasPrefix(opts.RefFullName, git.RefsTags) {
 		opType = ACTION_PUSH_TAG
 	} else {
 		// if not the first commit, set the compare URL.
@@ -504,7 +504,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 		return fmt.Errorf("Marshal: %v", err)
 	}
 
-	refName := git.RefEndName(opts.RefFullName)
+	refName := git.RefShortName(opts.RefFullName)
 	action := &Action{
 		ActUserID:    pusher.ID,
 		ActUserName:  pusher.Name,

+ 0 - 194
internal/db/git_diff.go

@@ -1,194 +0,0 @@
-// 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 db
-
-import (
-	"bytes"
-	"fmt"
-	"html"
-	"html/template"
-	"io"
-
-	"github.com/sergi/go-diff/diffmatchpatch"
-	"golang.org/x/net/html/charset"
-	"golang.org/x/text/transform"
-
-	"github.com/gogs/git-module"
-
-	"gogs.io/gogs/internal/conf"
-	"gogs.io/gogs/internal/template/highlight"
-	"gogs.io/gogs/internal/tool"
-)
-
-type DiffSection struct {
-	*git.DiffSection
-}
-
-var (
-	addedCodePrefix   = []byte("<span class=\"added-code\">")
-	removedCodePrefix = []byte("<span class=\"removed-code\">")
-	codeTagSuffix     = []byte("</span>")
-)
-
-func diffToHTML(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) template.HTML {
-	buf := bytes.NewBuffer(nil)
-
-	// Reproduce signs which are cutted for inline diff before.
-	switch lineType {
-	case git.DIFF_LINE_ADD:
-		buf.WriteByte('+')
-	case git.DIFF_LINE_DEL:
-		buf.WriteByte('-')
-	}
-
-	for i := range diffs {
-		switch {
-		case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DIFF_LINE_ADD:
-			buf.Write(addedCodePrefix)
-			buf.WriteString(html.EscapeString(diffs[i].Text))
-			buf.Write(codeTagSuffix)
-		case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DIFF_LINE_DEL:
-			buf.Write(removedCodePrefix)
-			buf.WriteString(html.EscapeString(diffs[i].Text))
-			buf.Write(codeTagSuffix)
-		case diffs[i].Type == diffmatchpatch.DiffEqual:
-			buf.WriteString(html.EscapeString(diffs[i].Text))
-		}
-	}
-
-	return template.HTML(buf.Bytes())
-}
-
-var diffMatchPatch = diffmatchpatch.New()
-
-func init() {
-	diffMatchPatch.DiffEditCost = 100
-}
-
-// ComputedInlineDiffFor computes inline diff for the given line.
-func (diffSection *DiffSection) ComputedInlineDiffFor(diffLine *git.DiffLine) template.HTML {
-	if conf.Git.DisableDiffHighlight {
-		return template.HTML(html.EscapeString(diffLine.Content[1:]))
-	}
-	var (
-		compareDiffLine *git.DiffLine
-		diff1           string
-		diff2           string
-	)
-
-	// try to find equivalent diff line. ignore, otherwise
-	switch diffLine.Type {
-	case git.DIFF_LINE_ADD:
-		compareDiffLine = diffSection.Line(git.DIFF_LINE_DEL, diffLine.RightIdx)
-		if compareDiffLine == nil {
-			return template.HTML(html.EscapeString(diffLine.Content))
-		}
-		diff1 = compareDiffLine.Content
-		diff2 = diffLine.Content
-	case git.DIFF_LINE_DEL:
-		compareDiffLine = diffSection.Line(git.DIFF_LINE_ADD, diffLine.LeftIdx)
-		if compareDiffLine == nil {
-			return template.HTML(html.EscapeString(diffLine.Content))
-		}
-		diff1 = diffLine.Content
-		diff2 = compareDiffLine.Content
-	default:
-		return template.HTML(html.EscapeString(diffLine.Content))
-	}
-
-	diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true)
-	diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
-
-	return diffToHTML(diffRecord, diffLine.Type)
-}
-
-type DiffFile struct {
-	*git.DiffFile
-	Sections []*DiffSection
-}
-
-func (diffFile *DiffFile) HighlightClass() string {
-	return highlight.FileNameToHighlightClass(diffFile.Name)
-}
-
-type Diff struct {
-	*git.Diff
-	Files []*DiffFile
-}
-
-func NewDiff(gitDiff *git.Diff) *Diff {
-	diff := &Diff{
-		Diff:  gitDiff,
-		Files: make([]*DiffFile, gitDiff.NumFiles()),
-	}
-
-	// FIXME: detect encoding while parsing.
-	var buf bytes.Buffer
-	for i := range gitDiff.Files {
-		buf.Reset()
-
-		diff.Files[i] = &DiffFile{
-			DiffFile: gitDiff.Files[i],
-			Sections: make([]*DiffSection, gitDiff.Files[i].NumSections()),
-		}
-
-		for j := range gitDiff.Files[i].Sections {
-			diff.Files[i].Sections[j] = &DiffSection{
-				DiffSection: gitDiff.Files[i].Sections[j],
-			}
-
-			for k := range diff.Files[i].Sections[j].Lines {
-				buf.WriteString(diff.Files[i].Sections[j].Lines[k].Content)
-				buf.WriteString("\n")
-			}
-		}
-
-		charsetLabel, err := tool.DetectEncoding(buf.Bytes())
-		if charsetLabel != "UTF-8" && err == nil {
-			encoding, _ := charset.Lookup(charsetLabel)
-			if encoding != nil {
-				d := encoding.NewDecoder()
-				for j := range diff.Files[i].Sections {
-					for k := range diff.Files[i].Sections[j].Lines {
-						if c, _, err := transform.String(d, diff.Files[i].Sections[j].Lines[k].Content); err == nil {
-							diff.Files[i].Sections[j].Lines[k].Content = c
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return diff
-}
-
-func ParsePatch(maxLines, maxLineCharacteres, maxFiles int, reader io.Reader) (*Diff, error) {
-	done := make(chan error)
-	var gitDiff *git.Diff
-	go func() {
-		gitDiff = git.ParsePatch(done, maxLines, maxLineCharacteres, maxFiles, reader)
-	}()
-
-	if err := <-done; err != nil {
-		return nil, fmt.Errorf("ParsePatch: %v", err)
-	}
-	return NewDiff(gitDiff), nil
-}
-
-func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
-	gitDiff, err := git.GetDiffRange(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacteres, maxFiles)
-	if err != nil {
-		return nil, fmt.Errorf("GetDiffRange: %v", err)
-	}
-	return NewDiff(gitDiff), nil
-}
-
-func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacteres, maxFiles int) (*Diff, error) {
-	gitDiff, err := git.GetDiffCommit(repoPath, commitID, maxLines, maxLineCharacteres, maxFiles)
-	if err != nil {
-		return nil, fmt.Errorf("GetDiffCommit: %v", err)
-	}
-	return NewDiff(gitDiff), nil
-}

+ 0 - 35
internal/db/git_diff_test.go

@@ -1,35 +0,0 @@
-// 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 db
-
-import (
-	"html/template"
-	"testing"
-
-	"github.com/gogs/git-module"
-	dmp "github.com/sergi/go-diff/diffmatchpatch"
-)
-
-func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
-	if s1 != string(s2) {
-		t.Errorf("%s should be equal %s", s2, s1)
-	}
-}
-
-func Test_diffToHTML(t *testing.T) {
-	assertEqual(t, "+foo <span class=\"added-code\">bar</span> biz", diffToHTML([]dmp.Diff{
-		{Type: dmp.DiffEqual, Text: "foo "},
-		{Type: dmp.DiffInsert, Text: "bar"},
-		{Type: dmp.DiffDelete, Text: " baz"},
-		{Type: dmp.DiffEqual, Text: " biz"},
-	}, git.DiffLineAdd))
-
-	assertEqual(t, "-foo <span class=\"removed-code\">bar</span> biz", diffToHTML([]dmp.Diff{
-		{Type: dmp.DiffEqual, Text: "foo "},
-		{Type: dmp.DiffDelete, Text: "bar"},
-		{Type: dmp.DiffInsert, Text: " baz"},
-		{Type: dmp.DiffEqual, Text: " biz"},
-	}, git.DiffLineDel))
-}

+ 5 - 5
internal/db/migrations/v16.go

@@ -18,7 +18,7 @@ import (
 )
 
 func updateRepositorySizes(x *xorm.Engine) (err error) {
-	log.Info("This migration could take up to minutes, please be patient.")
+	log.Info("[migrations.v16] This migration could take up to minutes, please be patient.")
 	type Repository struct {
 		ID      int64
 		OwnerID int64
@@ -41,7 +41,7 @@ func updateRepositorySizes(x *xorm.Engine) (err error) {
 			Find(&repos); err != nil {
 			return fmt.Errorf("select repos [offset: %d]: %v", offset, err)
 		}
-		log.Trace("Select [offset: %d, repos: %d]", offset, len(repos))
+		log.Trace("[migrations.v16] Select [offset: %d, repos: %d]", offset, len(repos))
 		if len(repos) == 0 {
 			break
 		}
@@ -60,10 +60,10 @@ func updateRepositorySizes(x *xorm.Engine) (err error) {
 				continue
 			}
 
-			repoPath := filepath.Join(conf.Repository.Root, strings.ToLower(user.Name), strings.ToLower(repo.Name)) + ".git"
-			countObject, err := git.GetRepoSize(repoPath)
+			repoPath := strings.ToLower(filepath.Join(conf.Repository.Root, user.Name, repo.Name)) + ".git"
+			countObject, err := git.RepoCountObjects(repoPath)
 			if err != nil {
-				log.Warn("GetRepoSize: %v", err)
+				log.Warn("[migrations.v16] Count repository objects: %v", err)
 				continue
 			}
 

+ 52 - 103
internal/db/mirror.go

@@ -5,7 +5,6 @@
 package db
 
 import (
-	"container/list"
 	"fmt"
 	"net/url"
 	"strings"
@@ -72,54 +71,17 @@ func (m *Mirror) ScheduleNextSync() {
 	m.NextSync = time.Now().Add(time.Duration(m.Interval) * time.Hour)
 }
 
-// findPasswordInMirrorAddress returns start (inclusive) and end index (exclusive)
-// of password portion of credentials in given mirror address.
-// It returns a boolean value to indicate whether password portion is found.
-func findPasswordInMirrorAddress(addr string) (start int, end int, found bool) {
-	// Find end of credentials (start of path)
-	end = strings.LastIndex(addr, "@")
-	if end == -1 {
-		return -1, -1, false
-	}
-
-	// Find delimiter of credentials (end of username)
-	start = strings.Index(addr, "://")
-	if start == -1 {
-		return -1, -1, false
-	}
-	start += 3
-	delim := strings.Index(addr[start:], ":")
-	if delim == -1 {
-		return -1, -1, false
-	}
-	delim += 1
-
-	if start+delim >= end {
-		return -1, -1, false // No password portion presented
-	}
-
-	return start + delim, end, true
-}
-
-// unescapeMirrorCredentials returns mirror address with unescaped credentials.
-func unescapeMirrorCredentials(addr string) string {
-	start, end, found := findPasswordInMirrorAddress(addr)
-	if !found {
-		return addr
-	}
-
-	password, _ := url.QueryUnescape(addr[start:end])
-	return addr[:start] + password + addr[end:]
-}
-
 func (m *Mirror) readAddress() {
 	if len(m.address) > 0 {
 		return
 	}
 
-	cfg, err := ini.Load(m.Repo.GitConfigPath())
+	cfg, err := ini.LoadSources(
+		ini.LoadOptions{IgnoreInlineComment: true},
+		m.Repo.GitConfigPath(),
+	)
 	if err != nil {
-		log.Error("Load: %v", err)
+		log.Error("load config: %v", err)
 		return
 	}
 	m.address = cfg.Section("remote \"origin\"").Key("url").Value()
@@ -128,6 +90,7 @@ func (m *Mirror) readAddress() {
 // HandleMirrorCredentials replaces user credentials from HTTP/HTTPS URL
 // with placeholder <credentials>.
 // It returns original string if protocol is not HTTP/HTTPS.
+// TODO(unknwon): Use url.Parse.
 func HandleMirrorCredentials(url string, mosaics bool) string {
 	i := strings.Index(url, "@")
 	if i == -1 {
@@ -161,34 +124,21 @@ func (m *Mirror) RawAddress() string {
 	return m.address
 }
 
-// FullAddress returns mirror address from Git repository config with unescaped credentials.
-func (m *Mirror) FullAddress() string {
-	m.readAddress()
-	return unescapeMirrorCredentials(m.address)
-}
-
-// escapeCredentials returns mirror address with escaped credentials.
-func escapeMirrorCredentials(addr string) string {
-	start, end, found := findPasswordInMirrorAddress(addr)
-	if !found {
-		return addr
-	}
-
-	return addr[:start] + url.QueryEscape(addr[start:end]) + addr[end:]
-}
-
 // SaveAddress writes new address to Git repository config.
 func (m *Mirror) SaveAddress(addr string) error {
 	repoPath := m.Repo.RepoPath()
 
-	err := git.RemoveRemote(repoPath, "origin")
+	err := git.RepoRemoveRemote(repoPath, "origin")
 	if err != nil {
 		return fmt.Errorf("remove remote 'origin': %v", err)
 	}
 
-	err = git.AddRemote(repoPath, "origin", addr, git.AddRemoteOptions{
-		Mirror: true,
-	})
+	addrURL, err := url.Parse(addr)
+	if err != nil {
+		return err
+	}
+
+	err = git.RepoAddRemote(repoPath, "origin", addrURL.String(), git.AddRemoteOptions{MirrorFetch: true})
 	if err != nil {
 		return fmt.Errorf("add remote 'origin': %v", err)
 	}
@@ -196,7 +146,7 @@ func (m *Mirror) SaveAddress(addr string) error {
 	return nil
 }
 
-const GIT_SHORT_EMPTY_SHA = "0000000"
+const gitShortEmptyID = "0000000"
 
 // mirrorSyncResult contains information of a updated reference.
 // If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
@@ -223,12 +173,12 @@ func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
 		case strings.HasPrefix(lines[i], " * "): // New reference
 			results = append(results, &mirrorSyncResult{
 				refName:     refName,
-				oldCommitID: GIT_SHORT_EMPTY_SHA,
+				oldCommitID: gitShortEmptyID,
 			})
 		case strings.HasPrefix(lines[i], " - "): // Delete reference
 			results = append(results, &mirrorSyncResult{
 				refName:     refName,
-				newCommitID: GIT_SHORT_EMPTY_SHA,
+				newCommitID: gitShortEmptyID,
 			})
 		case strings.HasPrefix(lines[i], "   "): // New commits of a reference
 			delimIdx := strings.Index(lines[i][3:], " ")
@@ -262,10 +212,7 @@ func (m *Mirror) runSync() ([]*mirrorSyncResult, bool) {
 
 	// Do a fast-fail testing against on repository URL to ensure it is accessible under
 	// good condition to prevent long blocking on URL resolution without syncing anything.
-	if !git.IsRepoURLAccessible(git.NetworkOptions{
-		URL:     m.RawAddress(),
-		Timeout: 10 * time.Second,
-	}) {
+	if !git.IsURLAccessible(time.Minute, m.RawAddress()) {
 		desc := fmt.Sprintf("Source URL of mirror repository '%s' is not accessible: %s", m.Repo.FullName(), m.MosaicsAddress())
 		if err := CreateRepositoryNotice(desc); err != nil {
 			log.Error("CreateRepositoryNotice: %v", err)
@@ -393,15 +340,14 @@ func SyncMirrors() {
 		// - Create "Mirror Sync" webhook event
 		// - Create mirror sync (create, push and delete) events and trigger the "mirror sync" webhooks
 
-		var gitRepo *git.Repository
 		if len(results) == 0 {
 			log.Trace("SyncMirrors [repo_id: %d]: no commits fetched", m.RepoID)
-		} else {
-			gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
-			if err != nil {
-				log.Error("OpenRepository [%d]: %v", m.RepoID, err)
-				continue
-			}
+		}
+
+		gitRepo, err := git.Open(m.Repo.RepoPath())
+		if err != nil {
+			log.Error("Failed to open repository [repo_id: %d]: %v", m.RepoID, err)
+			continue
 		}
 
 		for _, result := range results {
@@ -411,7 +357,7 @@ func SyncMirrors() {
 			}
 
 			// Delete reference
-			if result.newCommitID == GIT_SHORT_EMPTY_SHA {
+			if result.newCommitID == gitShortEmptyID {
 				if err = MirrorSyncDeleteAction(m.Repo, result.refName); err != nil {
 					log.Error("MirrorSyncDeleteAction [repo_id: %d]: %v", m.RepoID, err)
 				}
@@ -420,7 +366,7 @@ func SyncMirrors() {
 
 			// New reference
 			isNewRef := false
-			if result.oldCommitID == GIT_SHORT_EMPTY_SHA {
+			if result.oldCommitID == gitShortEmptyID {
 				if err = MirrorSyncCreateAction(m.Repo, result.refName); err != nil {
 					log.Error("MirrorSyncCreateAction [repo_id: %d]: %v", m.RepoID, err)
 					continue
@@ -429,49 +375,52 @@ func SyncMirrors() {
 			}
 
 			// Push commits
-			var commits *list.List
+			var commits []*git.Commit
 			var oldCommitID string
 			var newCommitID string
 			if !isNewRef {
-				oldCommitID, err = git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
+				oldCommitID, err = gitRepo.RevParse(result.oldCommitID)
 				if err != nil {
-					log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
+					log.Error("Failed to parse revision [repo_id: %d, old_commit_id: %s]: %v", m.RepoID, result.oldCommitID, err)
 					continue
 				}
-				newCommitID, err = git.GetFullCommitID(gitRepo.Path, result.newCommitID)
+				newCommitID, err = gitRepo.RevParse(result.newCommitID)
 				if err != nil {
-					log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
+					log.Error("Failed to parse revision [repo_id: %d, new_commit_id: %s]: %v", m.RepoID, result.newCommitID, err)
 					continue
 				}
-				commits, err = gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
+				commits, err = gitRepo.RevList([]string{oldCommitID + "..." + newCommitID})
 				if err != nil {
-					log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
+					log.Error("Failed to list commits [repo_id: %d, old_commit_id: %s, new_commit_id: %s]: %v", m.RepoID, oldCommitID, newCommitID, err)
 					continue
 				}
-			} else {
-				refNewCommitID, err := gitRepo.GetBranchCommitID(result.refName)
+
+			} else if gitRepo.HasBranch(result.refName) {
+				refNewCommit, err := gitRepo.BranchCommit(result.refName)
 				if err != nil {
-					log.Error("GetFullCommitID [%d]: %v", m.RepoID, err)
+					log.Error("Failed to get branch commit [repo_id: %d, branch: %s]: %v", m.RepoID, result.refName, err)
 					continue
 				}
-				if newCommit, err := gitRepo.GetCommit(refNewCommitID); err != nil {
-					log.Error("GetCommit [repo_id: %d, commit_id: %s]: %v", m.RepoID, refNewCommitID, err)
+
+				// TODO(unknwon): Get the commits for the new ref until the closest ancestor branch like GitHub does.
+				commits, err = refNewCommit.Ancestors(git.LogOptions{MaxCount: 9})
+				if err != nil {
+					log.Error("Failed to get ancestors [repo_id: %d, commit_id: %s]: %v", m.RepoID, refNewCommit.ID, err)
 					continue
-				} else {
-					// TODO: Get the commits for the new ref until the closest ancestor branch like Github does
-					commits, err = newCommit.CommitsBeforeLimit(10)
-					if err != nil {
-						log.Error("CommitsBeforeLimit [repo_id: %d, commit_id: %s]: %v", m.RepoID, refNewCommitID, err)
-					}
-					oldCommitID = git.EMPTY_SHA
-					newCommitID = refNewCommitID
 				}
+
+				// Put the latest commit in front of ancestors
+				commits = append([]*git.Commit{refNewCommit}, commits...)
+
+				oldCommitID = git.EmptyID
+				newCommitID = refNewCommit.ID.String()
 			}
+
 			if err = MirrorSyncPushAction(m.Repo, MirrorSyncPushActionOptions{
 				RefName:     result.refName,
 				OldCommitID: oldCommitID,
 				NewCommitID: newCommitID,
-				Commits:     ListToPushCommits(commits),
+				Commits:     CommitsToPushCommits(commits),
 			}); err != nil {
 				log.Error("MirrorSyncPushAction [repo_id: %d]: %v", m.RepoID, err)
 				continue
@@ -485,15 +434,15 @@ func SyncMirrors() {
 
 		// Get latest commit date and compare to current repository updated time,
 		// update if latest commit date is newer.
-		commitDate, err := git.GetLatestCommitDate(m.Repo.RepoPath(), "")
+		latestCommitTime, err := gitRepo.LatestCommitTime()
 		if err != nil {
 			log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err)
 			continue
-		} else if commitDate.Before(m.Repo.Updated) {
+		} else if !latestCommitTime.After(m.Repo.Updated) {
 			continue
 		}
 
-		if _, err = x.Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", commitDate.Unix(), m.RepoID); err != nil {
+		if _, err = x.Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", latestCommitTime.Unix(), m.RepoID); err != nil {
 			log.Error("Update 'repository.updated_unix' [%d]: %v", m.RepoID, err)
 			continue
 		}

+ 18 - 89
internal/db/mirror_test.go

@@ -7,102 +7,31 @@ package db
 import (
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
+	"github.com/stretchr/testify/assert"
 )
 
 func Test_parseRemoteUpdateOutput(t *testing.T) {
-	Convey("Parse mirror remote update output", t, func() {
-		testCases := []struct {
-			output  string
-			results []*mirrorSyncResult
-		}{
-			{
-				`
+	tests := []struct {
+		output     string
+		expResults []*mirrorSyncResult
+	}{
+		{
+			`
 From https://try.gogs.io/unknwon/upsteam
  * [new branch]      develop    -> develop
    b0bb24f..1d85a4f  master     -> master
  - [deleted]         (none)     -> bugfix
 `,
-				[]*mirrorSyncResult{
-					{"develop", GIT_SHORT_EMPTY_SHA, ""},
-					{"master", "b0bb24f", "1d85a4f"},
-					{"bugfix", "", GIT_SHORT_EMPTY_SHA},
-				},
+			[]*mirrorSyncResult{
+				{"develop", gitShortEmptyID, ""},
+				{"master", "b0bb24f", "1d85a4f"},
+				{"bugfix", "", gitShortEmptyID},
 			},
-		}
-
-		for _, tc := range testCases {
-			results := parseRemoteUpdateOutput(tc.output)
-			So(len(results), ShouldEqual, len(tc.results))
-
-			for i := range tc.results {
-				So(tc.results[i].refName, ShouldEqual, results[i].refName)
-				So(tc.results[i].oldCommitID, ShouldEqual, results[i].oldCommitID)
-				So(tc.results[i].newCommitID, ShouldEqual, results[i].newCommitID)
-			}
-		}
-	})
-}
-
-func Test_findPasswordInMirrorAddress(t *testing.T) {
-	Convey("Find password portion in mirror address", t, func() {
-		testCases := []struct {
-			addr       string
-			start, end int
-			found      bool
-			password   string
-		}{
-			{"http://localhost:3000/user/repo.git", -1, -1, false, ""},
-			{"http://user@localhost:3000/user/repo.git", -1, -1, false, ""},
-			{"http://user:@localhost:3000/user/repo.git", -1, -1, false, ""},
-			{"http://user:password@localhost:3000/user/repo.git", 12, 20, true, "password"},
-			{"http://username:my%3Asecure%3Bpassword@localhost:3000/user/repo.git", 16, 38, true, "my%3Asecure%3Bpassword"},
-			{"http://username:my%40secure%23password@localhost:3000/user/repo.git", 16, 38, true, "my%40secure%23password"},
-			{"http://username:@@localhost:3000/user/repo.git", 16, 17, true, "@"},
-		}
-
-		for _, tc := range testCases {
-			start, end, found := findPasswordInMirrorAddress(tc.addr)
-			So(start, ShouldEqual, tc.start)
-			So(end, ShouldEqual, tc.end)
-			So(found, ShouldEqual, tc.found)
-			if found {
-				So(tc.addr[start:end], ShouldEqual, tc.password)
-			}
-		}
-	})
-}
-
-func Test_unescapeMirrorCredentials(t *testing.T) {
-	Convey("Escape credentials in mirror address", t, func() {
-		testCases := []string{
-			"http://localhost:3000/user/repo.git", "http://localhost:3000/user/repo.git",
-			"http://user@localhost:3000/user/repo.git", "http://user@localhost:3000/user/repo.git",
-			"http://user:@localhost:3000/user/repo.git", "http://user:@localhost:3000/user/repo.git",
-			"http://user:password@localhost:3000/user/repo.git", "http://user:password@localhost:3000/user/repo.git",
-			"http://user:my%3Asecure%3Bpassword@localhost:3000/user/repo.git", "http://user:my:secure;password@localhost:3000/user/repo.git",
-			"http://user:my%40secure%23password@localhost:3000/user/repo.git", "http://user:my@secure#password@localhost:3000/user/repo.git",
-		}
-
-		for i := 0; i < len(testCases); i += 2 {
-			So(unescapeMirrorCredentials(testCases[i]), ShouldEqual, testCases[i+1])
-		}
-	})
-}
-
-func Test_escapeMirrorCredentials(t *testing.T) {
-	Convey("Escape credentials in mirror address", t, func() {
-		testCases := []string{
-			"http://localhost:3000/user/repo.git", "http://localhost:3000/user/repo.git",
-			"http://user@localhost:3000/user/repo.git", "http://user@localhost:3000/user/repo.git",
-			"http://user:@localhost:3000/user/repo.git", "http://user:@localhost:3000/user/repo.git",
-			"http://user:password@localhost:3000/user/repo.git", "http://user:password@localhost:3000/user/repo.git",
-			"http://user:my:secure;password@localhost:3000/user/repo.git", "http://user:my%3Asecure%3Bpassword@localhost:3000/user/repo.git",
-			"http://user:my@secure#password@localhost:3000/user/repo.git", "http://user:my%40secure%23password@localhost:3000/user/repo.git",
-		}
-
-		for i := 0; i < len(testCases); i += 2 {
-			So(escapeMirrorCredentials(testCases[i]), ShouldEqual, testCases[i+1])
-		}
-	})
+		},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.expResults, parseRemoteUpdateOutput(test.output))
+		})
+	}
 }

+ 58 - 43
internal/db/pull.go

@@ -7,7 +7,6 @@ package db
 import (
 	"fmt"
 	"os"
-	"path"
 	"path/filepath"
 	"strings"
 	"time"
@@ -212,9 +211,9 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
 	}
 
 	headRepoPath := RepoPath(pr.HeadUserName, pr.HeadRepo.Name)
-	headGitRepo, err := git.OpenRepository(headRepoPath)
+	headGitRepo, err := git.Open(headRepoPath)
 	if err != nil {
-		return fmt.Errorf("OpenRepository: %v", err)
+		return fmt.Errorf("open repository: %v", err)
 	}
 
 	// Create temporary directory to store temporary copy of the base repository,
@@ -228,7 +227,7 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
 	var stderr string
 	if _, stderr, err = process.ExecTimeout(5*time.Minute,
 		fmt.Sprintf("PullRequest.Merge (git clone): %s", tmpBasePath),
-		"git", "clone", "-b", pr.BaseBranch, baseGitRepo.Path, tmpBasePath); err != nil {
+		"git", "clone", "-b", pr.BaseBranch, baseGitRepo.Path(), tmpBasePath); err != nil {
 		return fmt.Errorf("git clone: %s", stderr)
 	}
 
@@ -311,13 +310,13 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
 	// Push changes on base branch to upstream.
 	if _, stderr, err = process.ExecDir(-1, tmpBasePath,
 		fmt.Sprintf("PullRequest.Merge (git push): %s", tmpBasePath),
-		"git", "push", baseGitRepo.Path, pr.BaseBranch); err != nil {
+		"git", "push", baseGitRepo.Path(), pr.BaseBranch); err != nil {
 		return fmt.Errorf("git push: %s", stderr)
 	}
 
-	pr.MergedCommitID, err = headGitRepo.GetBranchCommitID(pr.HeadBranch)
+	pr.MergedCommitID, err = headGitRepo.BranchCommitID(pr.HeadBranch)
 	if err != nil {
-		return fmt.Errorf("GetBranchCommit: %v", err)
+		return fmt.Errorf("get head branch %q commit ID: %v", pr.HeadBranch, err)
 	}
 
 	pr.HasMerged = true
@@ -351,42 +350,42 @@ func (pr *PullRequest) Merge(doer *User, baseGitRepo *git.Repository, mergeStyle
 		return nil
 	}
 
-	l, err := headGitRepo.CommitsBetweenIDs(pr.MergedCommitID, pr.MergeBase)
+	commits, err := headGitRepo.RevList([]string{pr.MergeBase + "..." + pr.MergedCommitID})
 	if err != nil {
-		log.Error("CommitsBetweenIDs: %v", err)
+		log.Error("Failed to list commits [merge_base: %s, merged_commit_id: %s]: %v", pr.MergeBase, pr.MergedCommitID, err)
 		return nil
 	}
 
-	// It is possible that head branch is not fully sync with base branch for merge commits,
-	// so we need to get latest head commit and append merge commit manully
-	// to avoid strange diff commits produced.
-	mergeCommit, err := baseGitRepo.GetBranchCommit(pr.BaseBranch)
+	// NOTE: It is possible that head branch is not fully sync with base branch
+	// for merge commits, so we need to get latest head commit and append merge
+	// commit manully to avoid strange diff commits produced.
+	mergeCommit, err := baseGitRepo.BranchCommit(pr.BaseBranch)
 	if err != nil {
-		log.Error("GetBranchCommit: %v", err)
+		log.Error("Failed to get base branch %q commit: %v", pr.BaseBranch, err)
 		return nil
 	}
 	if mergeStyle == MERGE_STYLE_REGULAR {
-		l.PushFront(mergeCommit)
+		commits = append([]*git.Commit{mergeCommit}, commits...)
 	}
 
-	commits, err := ListToPushCommits(l).ToApiPayloadCommits(pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
+	pcs, err := CommitsToPushCommits(commits).ToApiPayloadCommits(pr.BaseRepo.RepoPath(), pr.BaseRepo.HTMLURL())
 	if err != nil {
-		log.Error("ToApiPayloadCommits: %v", err)
+		log.Error("Failed to convert to API payload commits: %v", err)
 		return nil
 	}
 
 	p := &api.PushPayload{
-		Ref:        git.BRANCH_PREFIX + pr.BaseBranch,
+		Ref:        git.RefsHeads + pr.BaseBranch,
 		Before:     pr.MergeBase,
 		After:      mergeCommit.ID.String(),
 		CompareURL: conf.Server.ExternalURL + pr.BaseRepo.ComposeCompareURL(pr.MergeBase, pr.MergedCommitID),
-		Commits:    commits,
+		Commits:    pcs,
 		Repo:       pr.BaseRepo.APIFormat(nil),
 		Pusher:     pr.HeadRepo.MustOwner().APIFormat(),
 		Sender:     doer.APIFormat(),
 	}
 	if err = PrepareWebhooks(pr.BaseRepo, HOOK_EVENT_PUSH, p); err != nil {
-		log.Error("PrepareWebhooks: %v", err)
+		log.Error("Failed to prepare webhooks: %v", err)
 		return nil
 	}
 	return nil
@@ -599,36 +598,42 @@ func (pr *PullRequest) UpdatePatch() (err error) {
 		return nil
 	}
 
-	headGitRepo, err := git.OpenRepository(pr.HeadRepo.RepoPath())
+	headGitRepo, err := git.Open(pr.HeadRepo.RepoPath())
 	if err != nil {
-		return fmt.Errorf("OpenRepository: %v", err)
+		return fmt.Errorf("open repository: %v", err)
 	}
 
 	// Add a temporary remote.
 	tmpRemote := com.ToStr(time.Now().UnixNano())
-	if err = headGitRepo.AddRemote(tmpRemote, RepoPath(pr.BaseRepo.MustOwner().Name, pr.BaseRepo.Name), true); err != nil {
-		return fmt.Errorf("AddRemote: %v", err)
+	baseRepoPath := RepoPath(pr.BaseRepo.MustOwner().Name, pr.BaseRepo.Name)
+	err = headGitRepo.AddRemote(tmpRemote, baseRepoPath, git.AddRemoteOptions{Fetch: true})
+	if err != nil {
+		return fmt.Errorf("add remote %q [repo_id: %d]: %v", tmpRemote, pr.HeadRepoID, err)
 	}
 	defer func() {
-		headGitRepo.RemoveRemote(tmpRemote)
+		if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
+			log.Error("Failed to remove remote %q [repo_id: %d]: %v", tmpRemote, pr.HeadRepoID, err)
+		}
 	}()
+
 	remoteBranch := "remotes/" + tmpRemote + "/" + pr.BaseBranch
-	pr.MergeBase, err = headGitRepo.GetMergeBase(remoteBranch, pr.HeadBranch)
+	pr.MergeBase, err = headGitRepo.MergeBase(remoteBranch, pr.HeadBranch)
 	if err != nil {
-		return fmt.Errorf("GetMergeBase: %v", err)
+		return fmt.Errorf("get merge base: %v", err)
 	} else if err = pr.Update(); err != nil {
-		return fmt.Errorf("Update: %v", err)
+		return fmt.Errorf("update: %v", err)
 	}
 
-	patch, err := headGitRepo.GetPatch(pr.MergeBase, pr.HeadBranch)
+	patch, err := headGitRepo.DiffBinary(pr.MergeBase, pr.HeadBranch)
 	if err != nil {
-		return fmt.Errorf("GetPatch: %v", err)
+		return fmt.Errorf("get binary patch: %v", err)
 	}
 
 	if err = pr.BaseRepo.SavePatch(pr.Index, patch); err != nil {
-		return fmt.Errorf("BaseRepo.SavePatch: %v", err)
+		return fmt.Errorf("save patch: %v", err)
 	}
 
+	log.Trace("PullRequest[%d].UpdatePatch: patch saved", pr.ID)
 	return nil
 }
 
@@ -639,25 +644,35 @@ func (pr *PullRequest) PushToBaseRepo() (err error) {
 	log.Trace("PushToBaseRepo[%d]: pushing commits to base repo 'refs/pull/%d/head'", pr.BaseRepoID, pr.Index)
 
 	headRepoPath := pr.HeadRepo.RepoPath()
-	headGitRepo, err := git.OpenRepository(headRepoPath)
+	headGitRepo, err := git.Open(headRepoPath)
 	if err != nil {
-		return fmt.Errorf("OpenRepository: %v", err)
+		return fmt.Errorf("open repository: %v", err)
 	}
 
-	tmpRemoteName := fmt.Sprintf("tmp-pull-%d", pr.ID)
-	if err = headGitRepo.AddRemote(tmpRemoteName, pr.BaseRepo.RepoPath(), false); err != nil {
-		return fmt.Errorf("headGitRepo.AddRemote: %v", err)
+	tmpRemote := fmt.Sprintf("tmp-pull-%d", pr.ID)
+	if err = headGitRepo.AddRemote(tmpRemote, pr.BaseRepo.RepoPath()); err != nil {
+		return fmt.Errorf("add remote %q [repo_id: %d]: %v", tmpRemote, pr.HeadRepoID, err)
 	}
-	// Make sure to remove the remote even if the push fails
-	defer headGitRepo.RemoveRemote(tmpRemoteName)
 
-	headFile := fmt.Sprintf("refs/pull/%d/head", pr.Index)
+	// Make sure to remove the remote even if the push fails
+	defer func() {
+		if err := headGitRepo.RemoveRemote(tmpRemote); err != nil {
+			log.Error("Failed to remove remote %q [repo_id: %d]: %v", tmpRemote, pr.HeadRepoID, err)
+		}
+	}()
 
-	// Remove head in case there is a conflict.
-	os.Remove(path.Join(pr.BaseRepo.RepoPath(), headFile))
+	headRefspec := fmt.Sprintf("refs/pull/%d/head", pr.Index)
+	headFile := filepath.Join(pr.BaseRepo.RepoPath(), headRefspec)
+	if osutil.IsExist(headFile) {
+		err = os.Remove(headFile)
+		if err != nil {
+			return fmt.Errorf("remove head file [repo_id: %d]: %v", pr.BaseRepoID, err)
+		}
+	}
 
-	if err = git.Push(headRepoPath, tmpRemoteName, fmt.Sprintf("%s:%s", pr.HeadBranch, headFile)); err != nil {
-		return fmt.Errorf("Push: %v", err)
+	err = headGitRepo.Push(tmpRemote, fmt.Sprintf("%s:%s", pr.HeadBranch, headRefspec))
+	if err != nil {
+		return fmt.Errorf("push: %v", err)
 	}
 
 	return nil

+ 6 - 6
internal/db/release.go

@@ -119,10 +119,10 @@ func IsReleaseExist(repoID int64, tagName string) (bool, error) {
 func createTag(gitRepo *git.Repository, r *Release) error {
 	// Only actual create when publish.
 	if !r.IsDraft {
-		if !gitRepo.IsTagExist(r.TagName) {
-			commit, err := gitRepo.GetBranchCommit(r.Target)
+		if !gitRepo.HasTag(r.TagName) {
+			commit, err := gitRepo.BranchCommit(r.Target)
 			if err != nil {
-				return fmt.Errorf("GetBranchCommit: %v", err)
+				return fmt.Errorf("get branch commit: %v", err)
 			}
 
 			// Trim '--' prefix to prevent command line argument vulnerability.
@@ -134,15 +134,15 @@ func createTag(gitRepo *git.Repository, r *Release) error {
 				return err
 			}
 		} else {
-			commit, err := gitRepo.GetTagCommit(r.TagName)
+			commit, err := gitRepo.TagCommit(r.TagName)
 			if err != nil {
-				return fmt.Errorf("GetTagCommit: %v", err)
+				return fmt.Errorf("get tag commit: %v", err)
 			}
 
 			r.Sha1 = commit.ID.String()
 			r.NumCommits, err = commit.CommitsCount()
 			if err != nil {
-				return fmt.Errorf("CommitsCount: %v", err)
+				return fmt.Errorf("count commits: %v", err)
 			}
 		}
 	}

+ 61 - 56
internal/db/repo.go

@@ -119,9 +119,6 @@ func NewRepoContext() {
 	if version.Compare("1.8.3", conf.Git.Version, ">") {
 		log.Fatal("Gogs requires Git version greater or equal to 1.8.3")
 	}
-	git.HookDir = "custom_hooks"
-	git.HookSampleDir = "hooks"
-	git.DefaultCommitsPageSize = conf.UI.User.CommitsPagingNum
 
 	// Git requires setting user.name and user.email in order to commit changes.
 	for configKey, defaultValue := range map[string]string{"user.name": "Gogs", "user.email": "gogs@fake.local"} {
@@ -420,9 +417,9 @@ func (repo *Repository) mustOwner(e Engine) *User {
 }
 
 func (repo *Repository) UpdateSize() error {
-	countObject, err := git.GetRepoSize(repo.RepoPath())
+	countObject, err := git.RepoCountObjects(repo.RepoPath())
 	if err != nil {
-		return fmt.Errorf("GetRepoSize: %v", err)
+		return fmt.Errorf("count repository objects: %v", err)
 	}
 
 	repo.Size = countObject.Size + countObject.SizePack
@@ -602,33 +599,41 @@ func (repo *Repository) LocalCopyPath() string {
 // assume subsequent operations are against target branch when caller has confidence
 // about no race condition.
 func UpdateLocalCopyBranch(repoPath, localPath, branch string, isWiki bool) (err error) {
-	if !com.IsExist(localPath) {
+	if !osutil.IsExist(localPath) {
 		// Checkout to a specific branch fails when wiki is an empty repository.
 		if isWiki {
 			branch = ""
 		}
-		if err = git.Clone(repoPath, localPath, git.CloneRepoOptions{
-			Timeout: time.Duration(conf.Git.Timeout.Clone) * time.Second,
+		if err = git.Clone(repoPath, localPath, git.CloneOptions{
 			Branch:  branch,
+			Timeout: time.Duration(conf.Git.Timeout.Clone) * time.Second,
 		}); err != nil {
-			return fmt.Errorf("git clone %s: %v", branch, err)
-		}
-	} else {
-		if err = git.Fetch(localPath, git.FetchRemoteOptions{
-			Prune: true,
-		}); err != nil {
-			return fmt.Errorf("git fetch: %v", err)
-		}
-		if err = git.Checkout(localPath, git.CheckoutOptions{
-			Branch: branch,
-		}); err != nil {
-			return fmt.Errorf("git checkout %s: %v", branch, err)
+			return fmt.Errorf("git clone [branch: %s]: %v", branch, err)
 		}
+		return nil
+	}
 
-		// Reset to align with remote in case of force push.
-		if err = git.ResetHEAD(localPath, true, "origin/"+branch); err != nil {
-			return fmt.Errorf("git reset --hard origin/%s: %v", branch, err)
-		}
+	gitRepo, err := git.Open(localPath)
+	if err != nil {
+		return fmt.Errorf("open repository: %v", err)
+	}
+
+	if err = gitRepo.Fetch(git.FetchOptions{
+		Prune: true,
+	}); err != nil {
+		return fmt.Errorf("fetch: %v", err)
+	}
+
+	if err = gitRepo.Checkout(branch); err != nil {
+		return fmt.Errorf("checkout [branch: %s]: %v", branch, err)
+	}
+
+	// Reset to align with remote in case of force push.
+	rev := "origin/" + branch
+	if err = gitRepo.Reset(rev, git.ResetOptions{
+		Hard: true,
+	}); err != nil {
+		return fmt.Errorf("reset [revision: %s]: %v", rev, err)
 	}
 	return nil
 }
@@ -729,9 +734,7 @@ func wikiRemoteURL(remote string) string {
 	remote = strings.TrimSuffix(remote, ".git")
 	for _, suffix := range commonWikiURLSuffixes {
 		wikiURL := remote + suffix
-		if git.IsRepoURLAccessible(git.NetworkOptions{
-			URL: wikiURL,
-		}) {
+		if git.IsURLAccessible(time.Minute, wikiURL) {
 			return wikiURL
 		}
 	}
@@ -766,23 +769,23 @@ func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository,
 	migrateTimeout := time.Duration(conf.Git.Timeout.Migrate) * time.Second
 
 	RemoveAllWithNotice("Repository path erase before creation", repoPath)
-	if err = git.Clone(opts.RemoteAddr, repoPath, git.CloneRepoOptions{
+	if err = git.Clone(opts.RemoteAddr, repoPath, git.CloneOptions{
 		Mirror:  true,
 		Quiet:   true,
 		Timeout: migrateTimeout,
 	}); err != nil {
-		return repo, fmt.Errorf("Clone: %v", err)
+		return repo, fmt.Errorf("clone: %v", err)
 	}
 
 	wikiRemotePath := wikiRemoteURL(opts.RemoteAddr)
 	if len(wikiRemotePath) > 0 {
 		RemoveAllWithNotice("Repository wiki path erase before creation", wikiPath)
-		if err = git.Clone(wikiRemotePath, wikiPath, git.CloneRepoOptions{
+		if err = git.Clone(wikiRemotePath, wikiPath, git.CloneOptions{
 			Mirror:  true,
 			Quiet:   true,
 			Timeout: migrateTimeout,
 		}); err != nil {
-			log.Trace("Failed to clone wiki: %v", err)
+			log.Error("Failed to clone wiki: %v", err)
 			RemoveAllWithNotice("Delete repository wiki for initialization failure", wikiPath)
 		}
 	}
@@ -799,17 +802,15 @@ func MigrateRepository(doer, owner *User, opts MigrateRepoOptions) (*Repository,
 
 	if !repo.IsBare {
 		// Try to get HEAD branch and set it as default branch.
-		gitRepo, err := git.OpenRepository(repoPath)
+		gitRepo, err := git.Open(repoPath)
 		if err != nil {
-			return repo, fmt.Errorf("OpenRepository: %v", err)
+			return repo, fmt.Errorf("open repository: %v", err)
 		}
-		headBranch, err := gitRepo.GetHEADBranch()
+		refspec, err := gitRepo.SymbolicRef()
 		if err != nil {
-			return repo, fmt.Errorf("GetHEADBranch: %v", err)
-		}
-		if headBranch != nil {
-			repo.DefaultBranch = headBranch.Name
+			return repo, fmt.Errorf("get HEAD branch: %v", err)
 		}
+		repo.DefaultBranch = git.RefShortName(refspec)
 
 		if err = repo.UpdateSize(); err != nil {
 			log.Error("UpdateSize [repo_id: %d]: %v", repo.ID, err)
@@ -847,15 +848,15 @@ func cleanUpMigrateGitConfig(configPath string) error {
 	return nil
 }
 
-var hooksTpls = map[string]string{
+var hooksTpls = map[git.HookName]string{
 	"pre-receive":  "#!/usr/bin/env %s\n\"%s\" hook --config='%s' pre-receive\n",
 	"update":       "#!/usr/bin/env %s\n\"%s\" hook --config='%s' update $1 $2 $3\n",
 	"post-receive": "#!/usr/bin/env %s\n\"%s\" hook --config='%s' post-receive\n",
 }
 
 func createDelegateHooks(repoPath string) (err error) {
-	for _, name := range git.HookNames {
-		hookPath := filepath.Join(repoPath, "hooks", name)
+	for _, name := range git.ServerSideHooks {
+		hookPath := filepath.Join(repoPath, "hooks", string(name))
 		if err = ioutil.WriteFile(hookPath,
 			[]byte(fmt.Sprintf(hooksTpls[name], conf.Repository.ScriptType, conf.AppPath(), conf.CustomConf)),
 			os.ModePerm); err != nil {
@@ -1005,8 +1006,8 @@ func initRepository(e Engine, repoPath string, doer *User, repo *Repository, opt
 	}
 
 	// Init bare new repository.
-	if err = git.InitRepository(repoPath, true); err != nil {
-		return fmt.Errorf("InitRepository: %v", err)
+	if err = git.Init(repoPath, git.InitOptions{Bare: true}); err != nil {
+		return fmt.Errorf("init repository: %v", err)
 	} else if err = createDelegateHooks(repoPath); err != nil {
 		return fmt.Errorf("createDelegateHooks: %v", err)
 	}
@@ -1880,9 +1881,9 @@ func ReinitMissingRepositories() error {
 
 	for _, repo := range repos {
 		log.Trace("Initializing %d/%d...", repo.OwnerID, repo.ID)
-		if err := git.InitRepository(repo.RepoPath(), true); err != nil {
-			if err2 := CreateRepositoryNotice(fmt.Sprintf("InitRepository [%d]: %v", repo.ID, err)); err2 != nil {
-				return fmt.Errorf("CreateRepositoryNotice: %v", err)
+		if err := git.Init(repo.RepoPath(), git.InitOptions{Bare: true}); err != nil {
+			if err2 := CreateRepositoryNotice(fmt.Sprintf("init repository [repo_id: %d]: %v", repo.ID, err)); err2 != nil {
+				return fmt.Errorf("create repository notice: %v", err)
 			}
 		}
 	}
@@ -1930,7 +1931,11 @@ func GitFsck() {
 		func(idx int, bean interface{}) error {
 			repo := bean.(*Repository)
 			repoPath := repo.RepoPath()
-			if err := git.Fsck(repoPath, conf.Cron.RepoHealthCheck.Timeout, conf.Cron.RepoHealthCheck.Args...); err != nil {
+			err := git.RepoFsck(repoPath, git.FsckOptions{
+				Args:    conf.Cron.RepoHealthCheck.Args,
+				Timeout: conf.Cron.RepoHealthCheck.Timeout,
+			})
+			if err != nil {
 				desc := fmt.Sprintf("Failed to perform health check on repository '%s': %v", repoPath, err)
 				log.Warn(desc)
 				if err = CreateRepositoryNotice(desc); err != nil {
@@ -2441,24 +2446,24 @@ func (repo *Repository) GetForks() ([]*Repository, error) {
 //         \/             \/     \/     \/     \/
 //
 
-func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) {
+func (repo *Repository) CreateNewBranch(oldBranch, newBranch string) (err error) {
 	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
 	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
 
 	localPath := repo.LocalCopyPath()
 
-	if err = discardLocalRepoBranchChanges(localPath, oldBranchName); err != nil {
-		return fmt.Errorf("discardLocalRepoChanges: %v", err)
-	} else if err = repo.UpdateLocalCopyBranch(oldBranchName); err != nil {
-		return fmt.Errorf("UpdateLocalCopyBranch: %v", err)
+	if err = discardLocalRepoBranchChanges(localPath, oldBranch); err != nil {
+		return fmt.Errorf("discard changes in local copy [path: %s, branch: %s]: %v", localPath, oldBranch, err)
+	} else if err = repo.UpdateLocalCopyBranch(oldBranch); err != nil {
+		return fmt.Errorf("update branch for local copy [path: %s, branch: %s]: %v", localPath, oldBranch, err)
 	}
 
-	if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil {
-		return fmt.Errorf("CreateNewBranch: %v", err)
+	if err = repo.CheckoutNewBranch(oldBranch, newBranch); err != nil {
+		return fmt.Errorf("create new branch [base: %s, new: %s]: %v", oldBranch, newBranch, err)
 	}
 
-	if err = git.Push(localPath, "origin", branchName); err != nil {
-		return fmt.Errorf("Push: %v", err)
+	if err = git.RepoPush(localPath, "origin", newBranch); err != nil {
+		return fmt.Errorf("push [branch: %s]: %v", newBranch, err)
 	}
 
 	return nil

+ 14 - 14
internal/db/repo_branch.go

@@ -24,33 +24,33 @@ type Branch struct {
 }
 
 func GetBranchesByPath(path string) ([]*Branch, error) {
-	gitRepo, err := git.OpenRepository(path)
+	gitRepo, err := git.Open(path)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("open repository: %v", err)
 	}
 
-	brs, err := gitRepo.GetBranches()
+	names, err := gitRepo.Branches()
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("list branches")
 	}
 
-	branches := make([]*Branch, len(brs))
-	for i := range brs {
+	branches := make([]*Branch, len(names))
+	for i := range names {
 		branches[i] = &Branch{
 			RepoPath: path,
-			Name:     brs[i],
+			Name:     names[i],
 		}
 	}
 	return branches, nil
 }
 
-func (repo *Repository) GetBranch(br string) (*Branch, error) {
-	if !git.IsBranchExist(repo.RepoPath(), br) {
-		return nil, errors.ErrBranchNotExist{Name: br}
+func (repo *Repository) GetBranch(name string) (*Branch, error) {
+	if !git.RepoHasBranch(repo.RepoPath(), name) {
+		return nil, errors.ErrBranchNotExist{Name: name}
 	}
 	return &Branch{
 		RepoPath: repo.RepoPath(),
-		Name:     br,
+		Name:     name,
 	}, nil
 }
 
@@ -59,11 +59,11 @@ func (repo *Repository) GetBranches() ([]*Branch, error) {
 }
 
 func (br *Branch) GetCommit() (*git.Commit, error) {
-	gitRepo, err := git.OpenRepository(br.RepoPath)
+	gitRepo, err := git.Open(br.RepoPath)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("open repository: %v", err)
 	}
-	return gitRepo.GetBranchCommit(br.Name)
+	return gitRepo.BranchCommit(br.Name)
 }
 
 type ProtectBranchWhitelist struct {

+ 60 - 66
internal/db/repo_editor.go

@@ -23,6 +23,7 @@ import (
 
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db/errors"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/osutil"
 	"gogs.io/gogs/internal/process"
 	"gogs.io/gogs/internal/tool"
@@ -58,7 +59,7 @@ func ComposeHookEnvs(opts ComposeHookEnvsOptions) []string {
 		ENV_REPO_OWNER_SALT_MD5 + "=" + tool.MD5(opts.OwnerSalt),
 		ENV_REPO_ID + "=" + com.ToStr(opts.RepoID),
 		ENV_REPO_NAME + "=" + opts.RepoName,
-		ENV_REPO_CUSTOM_HOOKS_PATH + "=" + path.Join(opts.RepoPath, "custom_hooks"),
+		ENV_REPO_CUSTOM_HOOKS_PATH + "=" + filepath.Join(opts.RepoPath, "custom_hooks"),
 	}
 	return envs
 }
@@ -76,14 +77,15 @@ func discardLocalRepoBranchChanges(localPath, branch string) error {
 	if !com.IsExist(localPath) {
 		return nil
 	}
+
 	// No need to check if nothing in the repository.
-	if !git.IsBranchExist(localPath, branch) {
+	if !git.RepoHasBranch(localPath, branch) {
 		return nil
 	}
 
-	refName := "origin/" + branch
-	if err := git.ResetHEAD(localPath, true, refName); err != nil {
-		return fmt.Errorf("git reset --hard %s: %v", refName, err)
+	rev := "origin/" + branch
+	if err := git.RepoReset(localPath, rev, git.ResetOptions{Hard: true}); err != nil {
+		return fmt.Errorf("reset [revision: %s]: %v", rev, err)
 	}
 	return nil
 }
@@ -92,22 +94,17 @@ func (repo *Repository) DiscardLocalRepoBranchChanges(branch string) error {
 	return discardLocalRepoBranchChanges(repo.LocalCopyPath(), branch)
 }
 
-// checkoutNewBranch checks out to a new branch from the a branch name.
-func checkoutNewBranch(repoPath, localPath, oldBranch, newBranch string) error {
-	if err := git.Checkout(localPath, git.CheckoutOptions{
-		Timeout:   time.Duration(conf.Git.Timeout.Pull) * time.Second,
-		Branch:    newBranch,
-		OldBranch: oldBranch,
+// CheckoutNewBranch checks out to a new branch from the a branch name.
+func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error {
+	if err := git.RepoCheckout(repo.LocalCopyPath(), newBranch, git.CheckoutOptions{
+		BaseBranch: oldBranch,
+		Timeout:    time.Duration(conf.Git.Timeout.Pull) * time.Second,
 	}); err != nil {
-		return fmt.Errorf("git checkout -b %s %s: %v", newBranch, oldBranch, err)
+		return fmt.Errorf("checkout [base: %s, new: %s]: %v", oldBranch, newBranch, err)
 	}
 	return nil
 }
 
-func (repo *Repository) CheckoutNewBranch(oldBranch, newBranch string) error {
-	return checkoutNewBranch(repo.RepoPath(), repo.LocalCopyPath(), oldBranch, newBranch)
-}
-
 type UpdateRepoFileOptions struct {
 	LastCommitID string
 	OldBranch    string
@@ -135,16 +132,16 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
 
 	if opts.OldBranch != opts.NewBranch {
 		// Directly return error if new branch already exists in the server
-		if git.IsBranchExist(repoPath, opts.NewBranch) {
+		if git.RepoHasBranch(repoPath, opts.NewBranch) {
 			return errors.BranchAlreadyExists{Name: opts.NewBranch}
 		}
 
 		// Otherwise, delete branch from local copy in case out of sync
-		if git.IsBranchExist(localPath, opts.NewBranch) {
-			if err = git.DeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
+		if git.RepoHasBranch(localPath, opts.NewBranch) {
+			if err = git.RepoDeleteBranch(localPath, opts.NewBranch, git.DeleteBranchOptions{
 				Force: true,
 			}); err != nil {
-				return fmt.Errorf("delete branch[%s]: %v", opts.NewBranch, err)
+				return fmt.Errorf("delete branch %q: %v", opts.NewBranch, err)
 			}
 		}
 
@@ -167,7 +164,7 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
 	// Ignore move step if it's a new file under a directory.
 	// Otherwise, move the file when name changed.
 	if osutil.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
-		if err = git.MoveFile(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
+		if err = git.RepoMove(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
 			return fmt.Errorf("git mv %q %q: %v", opts.OldTreeName, opts.NewTreeName, err)
 		}
 	}
@@ -176,29 +173,28 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
 		return fmt.Errorf("write file: %v", err)
 	}
 
-	if err = git.AddChanges(localPath, true); err != nil {
+	if err = git.RepoAdd(localPath, git.AddOptions{All: true}); err != nil {
 		return fmt.Errorf("git add --all: %v", err)
-	} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
-		Committer: doer.NewGitSig(),
-		Message:   opts.Message,
-	}); err != nil {
+	} else if err = git.RepoCommit(localPath, doer.NewGitSig(), opts.Message); err != nil {
 		return fmt.Errorf("commit changes on %q: %v", localPath, err)
-	} else if err = git.PushWithEnvs(localPath, "origin", opts.NewBranch,
-		ComposeHookEnvs(ComposeHookEnvsOptions{
-			AuthUser:  doer,
-			OwnerName: repo.MustOwner().Name,
-			OwnerSalt: repo.MustOwner().Salt,
-			RepoID:    repo.ID,
-			RepoName:  repo.Name,
-			RepoPath:  repo.RepoPath(),
-		})); err != nil {
+	}
+
+	envs := ComposeHookEnvs(ComposeHookEnvsOptions{
+		AuthUser:  doer,
+		OwnerName: repo.MustOwner().Name,
+		OwnerSalt: repo.MustOwner().Salt,
+		RepoID:    repo.ID,
+		RepoName:  repo.Name,
+		RepoPath:  repo.RepoPath(),
+	})
+	if err = git.RepoPush(localPath, "origin", opts.NewBranch, git.PushOptions{Envs: envs}); err != nil {
 		return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
 	}
 	return nil
 }
 
 // GetDiffPreview produces and returns diff result of a file which is not yet committed.
-func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *Diff, err error) {
+func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *gitutil.Diff, err error) {
 	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
 	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
 
@@ -231,9 +227,9 @@ func (repo *Repository) GetDiffPreview(branch, treePath, content string) (diff *
 	pid := process.Add(fmt.Sprintf("GetDiffPreview [repo_path: %s]", repo.RepoPath()), cmd)
 	defer process.Remove(pid)
 
-	diff, err = ParsePatch(conf.Git.MaxGitDiffLines, conf.Git.MaxGitDiffLineCharacters, conf.Git.MaxGitDiffFiles, stdout)
+	diff, err = gitutil.ParseDiff(stdout, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars)
 	if err != nil {
-		return nil, fmt.Errorf("parse path: %v", err)
+		return nil, fmt.Errorf("parse diff: %v", err)
 	}
 
 	if err = cmd.Wait(); err != nil {
@@ -280,22 +276,21 @@ func (repo *Repository) DeleteRepoFile(doer *User, opts DeleteRepoFileOptions) (
 		return fmt.Errorf("remove file %q: %v", opts.TreePath, err)
 	}
 
-	if err = git.AddChanges(localPath, true); err != nil {
+	if err = git.RepoAdd(localPath, git.AddOptions{All: true}); err != nil {
 		return fmt.Errorf("git add --all: %v", err)
-	} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
-		Committer: doer.NewGitSig(),
-		Message:   opts.Message,
-	}); err != nil {
+	} else if err = git.RepoCommit(localPath, doer.NewGitSig(), opts.Message); err != nil {
 		return fmt.Errorf("commit changes to %q: %v", localPath, err)
-	} else if err = git.PushWithEnvs(localPath, "origin", opts.NewBranch,
-		ComposeHookEnvs(ComposeHookEnvsOptions{
-			AuthUser:  doer,
-			OwnerName: repo.MustOwner().Name,
-			OwnerSalt: repo.MustOwner().Salt,
-			RepoID:    repo.ID,
-			RepoName:  repo.Name,
-			RepoPath:  repo.RepoPath(),
-		})); err != nil {
+	}
+
+	envs := ComposeHookEnvs(ComposeHookEnvsOptions{
+		AuthUser:  doer,
+		OwnerName: repo.MustOwner().Name,
+		OwnerSalt: repo.MustOwner().Salt,
+		RepoID:    repo.ID,
+		RepoName:  repo.Name,
+		RepoPath:  repo.RepoPath(),
+	})
+	if err = git.RepoPush(localPath, "origin", opts.NewBranch, git.PushOptions{Envs: envs}); err != nil {
 		return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
 	}
 	return nil
@@ -496,22 +491,21 @@ func (repo *Repository) UploadRepoFiles(doer *User, opts UploadRepoFileOptions)
 		}
 	}
 
-	if err = git.AddChanges(localPath, true); err != nil {
+	if err = git.RepoAdd(localPath, git.AddOptions{All: true}); err != nil {
 		return fmt.Errorf("git add --all: %v", err)
-	} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
-		Committer: doer.NewGitSig(),
-		Message:   opts.Message,
-	}); err != nil {
+	} else if err = git.RepoCommit(localPath, doer.NewGitSig(), opts.Message); err != nil {
 		return fmt.Errorf("commit changes on %q: %v", localPath, err)
-	} else if err = git.PushWithEnvs(localPath, "origin", opts.NewBranch,
-		ComposeHookEnvs(ComposeHookEnvsOptions{
-			AuthUser:  doer,
-			OwnerName: repo.MustOwner().Name,
-			OwnerSalt: repo.MustOwner().Salt,
-			RepoID:    repo.ID,
-			RepoName:  repo.Name,
-			RepoPath:  repo.RepoPath(),
-		})); err != nil {
+	}
+
+	envs := ComposeHookEnvs(ComposeHookEnvsOptions{
+		AuthUser:  doer,
+		OwnerName: repo.MustOwner().Name,
+		OwnerSalt: repo.MustOwner().Salt,
+		RepoID:    repo.ID,
+		RepoName:  repo.Name,
+		RepoPath:  repo.RepoPath(),
+	})
+	if err = git.RepoPush(localPath, "origin", opts.NewBranch, git.PushOptions{Envs: envs}); err != nil {
 		return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
 	}
 

+ 23 - 28
internal/db/update.go

@@ -5,19 +5,18 @@
 package db
 
 import (
-	"container/list"
 	"fmt"
 	"os/exec"
 	"strings"
 
-	git "github.com/gogs/git-module"
+	"github.com/gogs/git-module"
 )
 
 // CommitToPushCommit transforms a git.Commit to PushCommit type.
 func CommitToPushCommit(commit *git.Commit) *PushCommit {
 	return &PushCommit{
 		Sha1:           commit.ID.String(),
-		Message:        commit.Message(),
+		Message:        commit.Message,
 		AuthorEmail:    commit.Author.Email,
 		AuthorName:     commit.Author.Name,
 		CommitterEmail: commit.Committer.Email,
@@ -26,27 +25,22 @@ func CommitToPushCommit(commit *git.Commit) *PushCommit {
 	}
 }
 
-func ListToPushCommits(l *list.List) *PushCommits {
-	if l == nil {
+func CommitsToPushCommits(commits []*git.Commit) *PushCommits {
+	if len(commits) == 0 {
 		return &PushCommits{}
 	}
 
-	commits := make([]*PushCommit, 0)
-	var actEmail string
-	for e := l.Front(); e != nil; e = e.Next() {
-		commit := e.Value.(*git.Commit)
-		if actEmail == "" {
-			actEmail = commit.Committer.Email
-		}
-		commits = append(commits, CommitToPushCommit(commit))
+	pcs := make([]*PushCommit, len(commits))
+	for i := range commits {
+		pcs[i] = CommitToPushCommit(commits[i])
 	}
-	return &PushCommits{l.Len(), commits, "", nil}
+	return &PushCommits{len(pcs), pcs, "", nil}
 }
 
 type PushUpdateOptions struct {
 	OldCommitID  string
 	NewCommitID  string
-	RefFullName  string
+	FullRefspec  string
 	PusherID     int64
 	PusherName   string
 	RepoUserName string
@@ -56,10 +50,10 @@ type PushUpdateOptions struct {
 // PushUpdate must be called for any push actions in order to
 // generates necessary push action history feeds.
 func PushUpdate(opts PushUpdateOptions) (err error) {
-	isNewRef := opts.OldCommitID == git.EMPTY_SHA
-	isDelRef := opts.NewCommitID == git.EMPTY_SHA
+	isNewRef := strings.HasPrefix(opts.OldCommitID, git.EmptyID)
+	isDelRef := strings.HasPrefix(opts.NewCommitID, git.EmptyID)
 	if isNewRef && isDelRef {
-		return fmt.Errorf("Old and new revisions are both %s", git.EMPTY_SHA)
+		return fmt.Errorf("both old and new revisions are %q", git.EmptyID)
 	}
 
 	repoPath := RepoPath(opts.RepoUserName, opts.RepoName)
@@ -70,9 +64,9 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
 		return fmt.Errorf("run 'git update-server-info': %v", err)
 	}
 
-	gitRepo, err := git.OpenRepository(repoPath)
+	gitRepo, err := git.Open(repoPath)
 	if err != nil {
-		return fmt.Errorf("OpenRepository: %v", err)
+		return fmt.Errorf("open repository: %v", err)
 	}
 
 	owner, err := GetUserByName(opts.RepoUserName)
@@ -90,12 +84,12 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
 	}
 
 	// Push tags
-	if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
+	if strings.HasPrefix(opts.FullRefspec, git.RefsTags) {
 		if err := CommitRepoAction(CommitRepoActionOptions{
 			PusherName:  opts.PusherName,
 			RepoOwnerID: owner.ID,
 			RepoName:    repo.Name,
-			RefFullName: opts.RefFullName,
+			RefFullName: opts.FullRefspec,
 			OldCommitID: opts.OldCommitID,
 			NewCommitID: opts.NewCommitID,
 			Commits:     &PushCommits{},
@@ -105,22 +99,23 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
 		return nil
 	}
 
-	var l *list.List
+	var commits []*git.Commit
 	// Skip read parent commits when delete branch
 	if !isDelRef {
 		// Push new branch
-		newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
+		newCommit, err := gitRepo.CatFileCommit(opts.NewCommitID)
 		if err != nil {
 			return fmt.Errorf("GetCommit [commit_id: %s]: %v", opts.NewCommitID, err)
 		}
 
 		if isNewRef {
-			l, err = newCommit.CommitsBeforeLimit(10)
+			commits, err = newCommit.Ancestors(git.LogOptions{MaxCount: 9})
 			if err != nil {
 				return fmt.Errorf("CommitsBeforeLimit [commit_id: %s]: %v", newCommit.ID, err)
 			}
+			commits = append([]*git.Commit{newCommit}, commits...)
 		} else {
-			l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
+			commits, err = newCommit.CommitsAfter(opts.OldCommitID)
 			if err != nil {
 				return fmt.Errorf("CommitsBeforeUntil [commit_id: %s]: %v", opts.OldCommitID, err)
 			}
@@ -131,10 +126,10 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
 		PusherName:  opts.PusherName,
 		RepoOwnerID: owner.ID,
 		RepoName:    repo.Name,
-		RefFullName: opts.RefFullName,
+		RefFullName: opts.FullRefspec,
 		OldCommitID: opts.OldCommitID,
 		NewCommitID: opts.NewCommitID,
-		Commits:     ListToPushCommits(l),
+		Commits:     CommitsToPushCommits(commits),
 	}); err != nil {
 		return fmt.Errorf("CommitRepoAction.(branch): %v", err)
 	}

+ 11 - 18
internal/db/user.go

@@ -6,7 +6,6 @@ package db
 
 import (
 	"bytes"
-	"container/list"
 	"crypto/sha256"
 	"crypto/subtle"
 	"encoding/hex"
@@ -977,28 +976,22 @@ func ValidateCommitWithEmail(c *git.Commit) *User {
 }
 
 // ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
-func ValidateCommitsWithEmails(oldCommits *list.List) *list.List {
-	var (
-		u          *User
-		emails     = map[string]*User{}
-		newCommits = list.New()
-		e          = oldCommits.Front()
-	)
-	for e != nil {
-		c := e.Value.(*git.Commit)
-
-		if v, ok := emails[c.Author.Email]; !ok {
-			u, _ = GetUserByEmail(c.Author.Email)
-			emails[c.Author.Email] = u
+func ValidateCommitsWithEmails(oldCommits []*git.Commit) []*UserCommit {
+	emails := make(map[string]*User)
+	newCommits := make([]*UserCommit, len(oldCommits))
+	for i := range oldCommits {
+		var u *User
+		if v, ok := emails[oldCommits[i].Author.Email]; !ok {
+			u, _ = GetUserByEmail(oldCommits[i].Author.Email)
+			emails[oldCommits[i].Author.Email] = u
 		} else {
 			u = v
 		}
 
-		newCommits.PushBack(UserCommit{
+		newCommits[i] = &UserCommit{
 			User:   u,
-			Commit: c,
-		})
-		e = e.Next()
+			Commit: oldCommits[i],
+		}
 	}
 	return newCommits
 }

+ 3 - 3
internal/db/webhook_dingtalk.go

@@ -86,7 +86,7 @@ func GetDingtalkPayload(p api.Payloader, event HookEventType) (payload *Dingtalk
 }
 
 func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 	refType := strings.Title(p.RefType)
 
 	actionCard := NewDingtalkActionCard("View "+refType, p.Repo.HTMLURL+"/src/"+refName)
@@ -99,7 +99,7 @@ func getDingtalkCreatePayload(p *api.CreatePayload) (*DingtalkPayload, error) {
 }
 
 func getDingtalkDeletePayload(p *api.DeletePayload) (*DingtalkPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 	refType := strings.Title(p.RefType)
 
 	actionCard := NewDingtalkActionCard("View Repo", p.Repo.HTMLURL)
@@ -122,7 +122,7 @@ func getDingtalkForkPayload(p *api.ForkPayload) (*DingtalkPayload, error) {
 }
 
 func getDingtalkPushPayload(p *api.PushPayload) (*DingtalkPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 
 	pusher := p.Pusher.FullName
 	if pusher == "" {

+ 3 - 3
internal/db/webhook_discord.go

@@ -71,7 +71,7 @@ func DiscordSHALinkFormatter(url string, text string) string {
 
 // getDiscordCreatePayload composes Discord payload for create new branch or tag.
 func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 	repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
 	content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
@@ -89,7 +89,7 @@ func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
 
 // getDiscordDeletePayload composes Discord payload for delete a branch or tag.
 func getDiscordDeletePayload(p *api.DeletePayload) (*DiscordPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 	repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	content := fmt.Sprintf("Deleted %s: %s/%s", p.RefType, repoLink, refName)
 	return &DiscordPayload{
@@ -124,7 +124,7 @@ func getDiscordForkPayload(p *api.ForkPayload) (*DiscordPayload, error) {
 func getDiscordPushPayload(p *api.PushPayload, slack *SlackMeta) (*DiscordPayload, error) {
 	// n new commits
 	var (
-		branchName   = git.RefEndName(p.Ref)
+		branchName   = git.RefShortName(p.Ref)
 		commitDesc   string
 		commitString string
 	)

+ 3 - 3
internal/db/webhook_slack.go

@@ -72,7 +72,7 @@ func SlackLinkFormatter(url string, text string) string {
 
 // getSlackCreatePayload composes Slack payload for create new branch or tag.
 func getSlackCreatePayload(p *api.CreatePayload) (*SlackPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	refLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
 	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
@@ -83,7 +83,7 @@ func getSlackCreatePayload(p *api.CreatePayload) (*SlackPayload, error) {
 
 // getSlackDeletePayload composes Slack payload for delete a branch or tag.
 func getSlackDeletePayload(p *api.DeletePayload) (*SlackPayload, error) {
-	refName := git.RefEndName(p.Ref)
+	refName := git.RefShortName(p.Ref)
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
 	return &SlackPayload{
@@ -104,7 +104,7 @@ func getSlackForkPayload(p *api.ForkPayload) (*SlackPayload, error) {
 func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
 	// n new commits
 	var (
-		branchName   = git.RefEndName(p.Ref)
+		branchName   = git.RefShortName(p.Ref)
 		commitDesc   string
 		commitString string
 	)

+ 14 - 20
internal/db/wiki.go

@@ -62,8 +62,8 @@ func (repo *Repository) InitWiki() error {
 		return nil
 	}
 
-	if err := git.InitRepository(repo.WikiPath(), true); err != nil {
-		return fmt.Errorf("InitRepository: %v", err)
+	if err := git.Init(repo.WikiPath(), git.InitOptions{Bare: true}); err != nil {
+		return fmt.Errorf("init repository: %v", err)
 	} else if err = createDelegateHooks(repo.WikiPath()); err != nil {
 		return fmt.Errorf("createDelegateHooks: %v", err)
 	}
@@ -125,15 +125,12 @@ func (repo *Repository) updateWikiPage(doer *User, oldTitle, title, content, mes
 	if len(message) == 0 {
 		message = "Update page '" + title + "'"
 	}
-	if err = git.AddChanges(localPath, true); err != nil {
-		return fmt.Errorf("AddChanges: %v", err)
-	} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
-		Committer: doer.NewGitSig(),
-		Message:   message,
-	}); err != nil {
-		return fmt.Errorf("CommitChanges: %v", err)
-	} else if err = git.Push(localPath, "origin", "master"); err != nil {
-		return fmt.Errorf("Push: %v", err)
+	if err = git.RepoAdd(localPath, git.AddOptions{All: true}); err != nil {
+		return fmt.Errorf("add all changes: %v", err)
+	} else if err = git.RepoCommit(localPath, doer.NewGitSig(), message); err != nil {
+		return fmt.Errorf("commit changes: %v", err)
+	} else if err = git.RepoPush(localPath, "origin", "master"); err != nil {
+		return fmt.Errorf("push: %v", err)
 	}
 
 	return nil
@@ -164,15 +161,12 @@ func (repo *Repository) DeleteWikiPage(doer *User, title string) (err error) {
 
 	message := "Delete page '" + title + "'"
 
-	if err = git.AddChanges(localPath, true); err != nil {
-		return fmt.Errorf("AddChanges: %v", err)
-	} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
-		Committer: doer.NewGitSig(),
-		Message:   message,
-	}); err != nil {
-		return fmt.Errorf("CommitChanges: %v", err)
-	} else if err = git.Push(localPath, "origin", "master"); err != nil {
-		return fmt.Errorf("Push: %v", err)
+	if err = git.RepoAdd(localPath, git.AddOptions{All: true}); err != nil {
+		return fmt.Errorf("add all changes: %v", err)
+	} else if err = git.RepoCommit(localPath, doer.NewGitSig(), message); err != nil {
+		return fmt.Errorf("commit changes: %v", err)
+	} else if err = git.RepoPush(localPath, "origin", "master"); err != nil {
+		return fmt.Errorf("push: %v", err)
 	}
 
 	return nil

+ 196 - 0
internal/gitutil/diff.go

@@ -0,0 +1,196 @@
+// 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 gitutil
+
+import (
+	"bytes"
+	"fmt"
+	"html"
+	"html/template"
+	"io"
+	"sync"
+
+	"github.com/sergi/go-diff/diffmatchpatch"
+	"golang.org/x/net/html/charset"
+	"golang.org/x/text/transform"
+
+	"github.com/gogs/git-module"
+
+	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/template/highlight"
+	"gogs.io/gogs/internal/tool"
+)
+
+// DiffSection is a wrapper to git.DiffSection with helper methods.
+type DiffSection struct {
+	*git.DiffSection
+
+	initOnce sync.Once
+	dmp      *diffmatchpatch.DiffMatchPatch
+}
+
+// ComputedInlineDiffFor computes inline diff for the given line.
+func (s *DiffSection) ComputedInlineDiffFor(line *git.DiffLine) template.HTML {
+	fallback := template.HTML(html.EscapeString(line.Content))
+	if conf.Git.DisableDiffHighlight {
+		return fallback
+	}
+
+	// Find equivalent diff line, ignore when not found.
+	var diff1, diff2 string
+	switch line.Type {
+	case git.DiffLineAdd:
+		compareLine := s.Line(git.DiffLineDelete, line.RightLine)
+		if compareLine == nil {
+			return fallback
+		}
+
+		diff1 = compareLine.Content
+		diff2 = line.Content
+
+	case git.DiffLineDelete:
+		compareLine := s.Line(git.DiffLineAdd, line.LeftLine)
+		if compareLine == nil {
+			return fallback
+		}
+
+		diff1 = line.Content
+		diff2 = compareLine.Content
+
+	default:
+		return fallback
+	}
+
+	s.initOnce.Do(func() {
+		s.dmp = diffmatchpatch.New()
+		s.dmp.DiffEditCost = 100
+	})
+
+	diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)
+	diffs = s.dmp.DiffCleanupEfficiency(diffs)
+
+	return diffsToHTML(diffs, line.Type)
+}
+
+func diffsToHTML(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) template.HTML {
+	buf := bytes.NewBuffer(nil)
+
+	// Reproduce signs which are cutted for inline diff before.
+	switch lineType {
+	case git.DiffLineAdd:
+		buf.WriteByte('+')
+	case git.DiffLineDelete:
+		buf.WriteByte('-')
+	}
+	buf.WriteByte(' ')
+
+	const (
+		addedCodePrefix   = `<span class="added-code">`
+		removedCodePrefix = `<span class="removed-code">`
+		codeTagSuffix     = `</span>`
+	)
+
+	for i := range diffs {
+		switch {
+		case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
+			buf.WriteString(addedCodePrefix)
+			buf.WriteString(html.EscapeString(diffs[i].Text))
+			buf.WriteString(codeTagSuffix)
+		case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
+			buf.WriteString(removedCodePrefix)
+			buf.WriteString(html.EscapeString(diffs[i].Text))
+			buf.WriteString(codeTagSuffix)
+		case diffs[i].Type == diffmatchpatch.DiffEqual:
+			buf.WriteString(html.EscapeString(diffs[i].Text))
+		}
+	}
+
+	return template.HTML(buf.Bytes())
+}
+
+// DiffFile is a wrapper to git.DiffFile with helper methods.
+type DiffFile struct {
+	*git.DiffFile
+	Sections []*DiffSection
+}
+
+// HighlightClass returns the detected highlight class for the file.
+func (diffFile *DiffFile) HighlightClass() string {
+	return highlight.FileNameToHighlightClass(diffFile.Name)
+}
+
+// Diff is a wrapper to git.Diff with helper methods.
+type Diff struct {
+	*git.Diff
+	Files []*DiffFile
+}
+
+// NewDiff returns a new wrapper of given git.Diff.
+func NewDiff(oldDiff *git.Diff) *Diff {
+	newDiff := &Diff{
+		Diff:  oldDiff,
+		Files: make([]*DiffFile, oldDiff.NumFiles()),
+	}
+
+	// FIXME: detect encoding while parsing.
+	var buf bytes.Buffer
+	for i := range oldDiff.Files {
+		buf.Reset()
+
+		newDiff.Files[i] = &DiffFile{
+			DiffFile: oldDiff.Files[i],
+			Sections: make([]*DiffSection, oldDiff.Files[i].NumSections()),
+		}
+
+		for j := range oldDiff.Files[i].Sections {
+			newDiff.Files[i].Sections[j] = &DiffSection{
+				DiffSection: oldDiff.Files[i].Sections[j],
+			}
+
+			for k := range newDiff.Files[i].Sections[j].Lines {
+				buf.WriteString(newDiff.Files[i].Sections[j].Lines[k].Content)
+				buf.WriteString("\n")
+			}
+		}
+
+		charsetLabel, err := tool.DetectEncoding(buf.Bytes())
+		if charsetLabel != "UTF-8" && err == nil {
+			encoding, _ := charset.Lookup(charsetLabel)
+			if encoding != nil {
+				d := encoding.NewDecoder()
+				for j := range newDiff.Files[i].Sections {
+					for k := range newDiff.Files[i].Sections[j].Lines {
+						if c, _, err := transform.String(d, newDiff.Files[i].Sections[j].Lines[k].Content); err == nil {
+							newDiff.Files[i].Sections[j].Lines[k].Content = c
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return newDiff
+}
+
+// ParseDiff parses the diff from given io.Reader.
+func ParseDiff(r io.Reader, maxFiles, maxFileLines, maxLineChars int) (*Diff, error) {
+	done := make(chan git.SteamParseDiffResult)
+	go git.StreamParseDiff(r, done, maxFiles, maxFileLines, maxLineChars)
+
+	result := <-done
+	if result.Err != nil {
+		return nil, fmt.Errorf("stream parse diff: %v", result.Err)
+	}
+	return NewDiff(result.Diff), nil
+}
+
+// RepoDiff parses the diff on given revisions of given repository.
+func RepoDiff(repo *git.Repository, rev string, maxFiles, maxFileLines, maxLineChars int, opts ...git.DiffOptions) (*Diff, error) {
+	diff, err := repo.Diff(rev, maxFiles, maxFileLines, maxLineChars, opts...)
+	if err != nil {
+		return nil, fmt.Errorf("get diff: %v", err)
+	}
+	return NewDiff(diff), nil
+}

+ 49 - 0
internal/gitutil/diff_test.go

@@ -0,0 +1,49 @@
+// 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 gitutil
+
+import (
+	"html/template"
+	"testing"
+
+	dmp "github.com/sergi/go-diff/diffmatchpatch"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/gogs/git-module"
+)
+
+func Test_diffsToHTML(t *testing.T) {
+	tests := []struct {
+		diffs    []dmp.Diff
+		lineType git.DiffLineType
+		expHTML  template.HTML
+	}{
+		{
+			diffs: []dmp.Diff{
+				{Type: dmp.DiffEqual, Text: "foo "},
+				{Type: dmp.DiffInsert, Text: "bar"},
+				{Type: dmp.DiffDelete, Text: " baz"},
+				{Type: dmp.DiffEqual, Text: " biz"},
+			},
+			lineType: git.DiffLineAdd,
+			expHTML:  template.HTML(`+ foo <span class="added-code">bar</span> biz`),
+		},
+		{
+			diffs: []dmp.Diff{
+				{Type: dmp.DiffEqual, Text: "foo "},
+				{Type: dmp.DiffDelete, Text: "bar"},
+				{Type: dmp.DiffInsert, Text: " baz"},
+				{Type: dmp.DiffEqual, Text: " biz"},
+			},
+			lineType: git.DiffLineDelete,
+			expHTML:  template.HTML(`- foo <span class="removed-code">bar</span> biz`),
+		},
+	}
+	for _, test := range tests {
+		t.Run("", func(t *testing.T) {
+			assert.Equal(t, test.expHTML, diffsToHTML(test.diffs, test.lineType))
+		})
+	}
+}

+ 19 - 0
internal/gitutil/error.go

@@ -0,0 +1,19 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"github.com/gogs/git-module"
+)
+
+// IsErrRevisionNotExist returns true if the error is git.ErrRevisionNotExist.
+func IsErrRevisionNotExist(err error) bool {
+	return err == git.ErrRevisionNotExist
+}
+
+// IsErrNoMergeBase returns true if the error is git.ErrNoMergeBase.
+func IsErrNoMergeBase(err error) bool {
+	return err == git.ErrNoMergeBase
+}

+ 23 - 0
internal/gitutil/error_test.go

@@ -0,0 +1,23 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"os"
+	"testing"
+
+	"github.com/gogs/git-module"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsErrRevisionNotExist(t *testing.T) {
+	assert.True(t, IsErrRevisionNotExist(git.ErrRevisionNotExist))
+	assert.False(t, IsErrRevisionNotExist(os.ErrNotExist))
+}
+
+func TestIsErrNoMergeBase(t *testing.T) {
+	assert.True(t, IsErrNoMergeBase(git.ErrNoMergeBase))
+	assert.False(t, IsErrNoMergeBase(os.ErrNotExist))
+}

+ 23 - 0
internal/gitutil/mock.go

@@ -0,0 +1,23 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"github.com/gogs/git-module"
+)
+
+type MockModuleStore struct {
+	RepoAddRemote    func(repoPath, name, url string, opts ...git.AddRemoteOptions) error
+	RepoDiffNameOnly func(repoPath, base, head string, opts ...git.DiffNameOnlyOptions) ([]string, error)
+	RepoLog          func(repoPath, rev string, opts ...git.LogOptions) ([]*git.Commit, error)
+	RepoMergeBase    func(repoPath, base, head string, opts ...git.MergeBaseOptions) (string, error)
+	RepoRemoveRemote func(repoPath, name string, opts ...git.RemoveRemoteOptions) error
+	RepoTags         func(repoPath string, opts ...git.TagsOptions) ([]string, error)
+}
+
+// MockModule holds mock implementation of each method for Modulers interface.
+// When the field is non-nil, it is considered mocked. Otherwise, the real
+// implementation will be executed.
+var MockModule MockModuleStore

+ 90 - 0
internal/gitutil/module.go

@@ -0,0 +1,90 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"github.com/gogs/git-module"
+)
+
+// Moduler is the interface for Git operations.
+//
+// NOTE: All methods are sorted in alphabetically.
+type Moduler interface {
+	// AddRemote adds a new remote to the repository in given path.
+	RepoAddRemote(repoPath, name, url string, opts ...git.AddRemoteOptions) error
+	// RepoDiffNameOnly returns a list of changed files between base and head revisions
+	// of the repository in given path.
+	RepoDiffNameOnly(repoPath, base, head string, opts ...git.DiffNameOnlyOptions) ([]string, error)
+	// RepoLog returns a list of commits in the state of given revision of the repository
+	// in given path. The returned list is in reverse chronological order.
+	RepoLog(repoPath, rev string, opts ...git.LogOptions) ([]*git.Commit, error)
+	// RepoMergeBase returns merge base between base and head revisions of the repository
+	// in given path.
+	RepoMergeBase(repoPath, base, head string, opts ...git.MergeBaseOptions) (string, error)
+	// RepoRemoveRemote removes a remote from the repository in given path.
+	RepoRemoveRemote(repoPath, name string, opts ...git.RemoveRemoteOptions) error
+	// RepoTags returns a list of tags of the repository in given path.
+	RepoTags(repoPath string, opts ...git.TagsOptions) ([]string, error)
+
+	Utiler
+}
+
+// Utiler is the interface for utility helpers implemented in this package.
+//
+// NOTE: All methods are sorted in alphabetically.
+type Utiler interface {
+	// GetPullRequestMeta gathers pull request metadata based on given head and base information.
+	PullRequestMeta(headPath, basePath, headBranch, baseBranch string) (*PullRequestMeta, error)
+	// ListTagsAfter returns a list of tags "after" (exlusive) given tag.
+	ListTagsAfter(repoPath, after string, limit int) (*TagsPage, error)
+}
+
+// moduler is holds real implementation.
+type moduler struct{}
+
+func (moduler) RepoAddRemote(repoPath, name, url string, opts ...git.AddRemoteOptions) error {
+	if MockModule.RepoAddRemote != nil {
+		return MockModule.RepoAddRemote(repoPath, name, url, opts...)
+	}
+	return git.RepoAddRemote(repoPath, name, url, opts...)
+}
+
+func (moduler) RepoDiffNameOnly(repoPath, base, head string, opts ...git.DiffNameOnlyOptions) ([]string, error) {
+	if MockModule.RepoDiffNameOnly != nil {
+		return MockModule.RepoDiffNameOnly(repoPath, base, head, opts...)
+	}
+	return git.RepoDiffNameOnly(repoPath, base, head, opts...)
+}
+
+func (moduler) RepoLog(repoPath, rev string, opts ...git.LogOptions) ([]*git.Commit, error) {
+	if MockModule.RepoLog != nil {
+		return MockModule.RepoLog(repoPath, rev, opts...)
+	}
+	return git.RepoLog(repoPath, rev, opts...)
+}
+
+func (moduler) RepoMergeBase(repoPath, base, head string, opts ...git.MergeBaseOptions) (string, error) {
+	if MockModule.RepoMergeBase != nil {
+		return MockModule.RepoMergeBase(repoPath, base, head, opts...)
+	}
+	return git.RepoMergeBase(repoPath, base, head, opts...)
+}
+
+func (moduler) RepoRemoveRemote(repoPath, name string, opts ...git.RemoveRemoteOptions) error {
+	if MockModule.RepoRemoveRemote != nil {
+		return MockModule.RepoRemoveRemote(repoPath, name, opts...)
+	}
+	return git.RepoRemoveRemote(repoPath, name, opts...)
+}
+
+func (moduler) RepoTags(repoPath string, opts ...git.TagsOptions) ([]string, error) {
+	if MockModule.RepoTags != nil {
+		return MockModule.RepoTags(repoPath, opts...)
+	}
+	return git.RepoTags(repoPath, opts...)
+}
+
+// Module is a mockable interface for Git operations.
+var Module Moduler = moduler{}

+ 69 - 0
internal/gitutil/pull_request.go

@@ -0,0 +1,69 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"fmt"
+	"strconv"
+	"time"
+
+	"github.com/gogs/git-module"
+	"github.com/pkg/errors"
+	log "unknwon.dev/clog/v2"
+)
+
+// PullRequestMeta contains metadata for a pull request.
+type PullRequestMeta struct {
+	// The merge base of the pull request.
+	MergeBase string
+	// The commits that are requested to be merged.
+	Commits []*git.Commit
+	// The number of files changed.
+	NumFiles int
+}
+
+func (moduler) PullRequestMeta(headPath, basePath, headBranch, baseBranch string) (*PullRequestMeta, error) {
+	tmpRemoteBranch := baseBranch
+
+	// We need to create a temporary remote when the pull request is sent from a forked repository.
+	if headPath != basePath {
+		tmpRemote := strconv.FormatInt(time.Now().UnixNano(), 10)
+		err := Module.RepoAddRemote(headPath, tmpRemote, basePath, git.AddRemoteOptions{Fetch: true})
+		if err != nil {
+			return nil, fmt.Errorf("add remote: %v", err)
+		}
+		defer func() {
+			err := Module.RepoRemoveRemote(headPath, tmpRemote)
+			if err != nil {
+				log.Error("Failed to remove remote %q [path: %s]: %v", tmpRemote, headPath, err)
+				return
+			}
+		}()
+
+		tmpRemoteBranch = "remotes/" + tmpRemote + "/" + baseBranch
+	}
+
+	mergeBase, err := Module.RepoMergeBase(headPath, tmpRemoteBranch, headBranch)
+	if err != nil {
+		return nil, errors.Wrap(err, "get merge base")
+	}
+
+	commits, err := Module.RepoLog(headPath, mergeBase+"..."+headBranch)
+	if err != nil {
+		return nil, errors.Wrap(err, "get commits")
+	}
+
+	// Count number of changed files
+	names, err := Module.RepoDiffNameOnly(headPath, tmpRemoteBranch, headBranch, git.DiffNameOnlyOptions{NeedsMergeBase: true})
+	if err != nil {
+		return nil, errors.Wrap(err, "get changed files")
+	}
+
+	return &PullRequestMeta{
+		MergeBase: mergeBase,
+		Commits:   commits,
+		NumFiles:  len(names),
+	}, nil
+}

+ 108 - 0
internal/gitutil/pull_request_test.go

@@ -0,0 +1,108 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/gogs/git-module"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestModuler_PullRequestMeta(t *testing.T) {
+	headPath := "/head/path"
+	basePath := "/base/path"
+	headBranch := "head_branch"
+	baseBranch := "base_branch"
+	mergeBase := "MERGE-BASE"
+	changedFiles := []string{"a.go", "b.txt"}
+	commits := []*git.Commit{
+		{ID: git.MustIDFromString("adfd6da3c0a3fb038393144becbf37f14f780087")},
+	}
+
+	MockModule.RepoAddRemote = func(repoPath, name, url string, opts ...git.AddRemoteOptions) error {
+		if repoPath != headPath {
+			return fmt.Errorf("repoPath: want %q but got %q", headPath, repoPath)
+		} else if name == "" {
+			return errors.New("empty name")
+		} else if url != basePath {
+			return fmt.Errorf("url: want %q but got %q", basePath, url)
+		}
+
+		if len(opts) == 0 {
+			return errors.New("no options")
+		} else if !opts[0].Fetch {
+			return fmt.Errorf("opts.Fetch: want %v but got %v", true, opts[0].Fetch)
+		}
+
+		return nil
+	}
+	MockModule.RepoMergeBase = func(repoPath, base, head string, opts ...git.MergeBaseOptions) (string, error) {
+		if repoPath != headPath {
+			return "", fmt.Errorf("repoPath: want %q but got %q", headPath, repoPath)
+		} else if base == "" {
+			return "", errors.New("empty base")
+		} else if head != headBranch {
+			return "", fmt.Errorf("head: want %q but got %q", headBranch, head)
+		}
+
+		return mergeBase, nil
+	}
+	MockModule.RepoLog = func(repoPath, rev string, opts ...git.LogOptions) ([]*git.Commit, error) {
+		if repoPath != headPath {
+			return nil, fmt.Errorf("repoPath: want %q but got %q", headPath, repoPath)
+		}
+
+		expRev := mergeBase + "..." + headBranch
+		if rev != expRev {
+			return nil, fmt.Errorf("rev: want %q but got %q", expRev, rev)
+		}
+
+		return commits, nil
+	}
+	MockModule.RepoDiffNameOnly = func(repoPath, base, head string, opts ...git.DiffNameOnlyOptions) ([]string, error) {
+		if repoPath != headPath {
+			return nil, fmt.Errorf("repoPath: want %q but got %q", headPath, repoPath)
+		} else if base == "" {
+			return nil, errors.New("empty base")
+		} else if head != headBranch {
+			return nil, fmt.Errorf("head: want %q but got %q", headBranch, head)
+		}
+
+		if len(opts) == 0 {
+			return nil, errors.New("no options")
+		} else if !opts[0].NeedsMergeBase {
+			return nil, fmt.Errorf("opts.NeedsMergeBase: want %v but got %v", true, opts[0].NeedsMergeBase)
+		}
+
+		return changedFiles, nil
+	}
+	MockModule.RepoRemoveRemote = func(repoPath, name string, opts ...git.RemoveRemoteOptions) error {
+		if repoPath != headPath {
+			return fmt.Errorf("repoPath: want %q but got %q", headPath, repoPath)
+		} else if name == "" {
+			return errors.New("empty name")
+		}
+
+		return nil
+	}
+	defer func() {
+		MockModule = MockModuleStore{}
+	}()
+
+	meta, err := Module.PullRequestMeta(headPath, basePath, headBranch, baseBranch)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expMeta := &PullRequestMeta{
+		MergeBase: mergeBase,
+		Commits:   commits,
+		NumFiles:  2,
+	}
+	assert.Equal(t, expMeta, meta)
+}

+ 48 - 0
internal/gitutil/submodule.go

@@ -0,0 +1,48 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"fmt"
+	"net/url"
+	"strings"
+
+	"github.com/gogs/git-module"
+
+	"gogs.io/gogs/internal/lazyregexp"
+)
+
+var scpSyntax = lazyregexp.New(`^([a-zA-Z0-9_]+@)?([a-zA-Z0-9._-]+):(.*)$`)
+
+// InferSubmoduleURL returns the inferred external URL of the submodule at best effort.
+func InferSubmoduleURL(mod *git.Submodule) string {
+	raw := strings.TrimSuffix(mod.URL, "/")
+	raw = strings.TrimSuffix(raw, ".git")
+
+	parsed, err := url.Parse(raw)
+	if err != nil {
+		// Try parse as SCP syntax again
+		match := scpSyntax.FindAllStringSubmatch(raw, -1)
+		if len(match) == 0 {
+			return mod.URL
+		}
+		parsed = &url.URL{
+			Scheme: "http",
+			Host:   match[0][2],
+			Path:   match[0][3],
+		}
+	}
+
+	switch parsed.Scheme {
+	case "http", "https":
+		raw = parsed.String()
+	case "ssh":
+		raw = fmt.Sprintf("http://%s%s", parsed.Host, parsed.Path)
+	default:
+		return raw
+	}
+
+	return fmt.Sprintf("%s/commit/%s", raw, mod.Commit)
+}

+ 58 - 0
internal/gitutil/submodule_test.go

@@ -0,0 +1,58 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"testing"
+
+	"github.com/gogs/git-module"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestInferSubmoduleURL(t *testing.T) {
+	tests := []struct {
+		name      string
+		submodule *git.Submodule
+		expURL    string
+	}{
+		{
+			name: "HTTPS URL",
+			submodule: &git.Submodule{
+				URL:    "https://github.com/gogs/docs-api.git",
+				Commit: "6b08f76a5313fa3d26859515b30aa17a5faa2807",
+			},
+			expURL: "https://github.com/gogs/docs-api/commit/6b08f76a5313fa3d26859515b30aa17a5faa2807",
+		},
+		{
+			name: "SSH URL with port",
+			submodule: &git.Submodule{
+				URL:    "ssh://user@github.com:22/gogs/docs-api.git",
+				Commit: "6b08f76a5313fa3d26859515b30aa17a5faa2807",
+			},
+			expURL: "http://github.com:22/gogs/docs-api/commit/6b08f76a5313fa3d26859515b30aa17a5faa2807",
+		},
+		{
+			name: "SSH URL in SCP syntax",
+			submodule: &git.Submodule{
+				URL:    "git@github.com:gogs/docs-api.git",
+				Commit: "6b08f76a5313fa3d26859515b30aa17a5faa2807",
+			},
+			expURL: "http://github.com/gogs/docs-api/commit/6b08f76a5313fa3d26859515b30aa17a5faa2807",
+		},
+		{
+			name: "bad URL",
+			submodule: &git.Submodule{
+				URL:    "ftp://example.com",
+				Commit: "6b08f76a5313fa3d26859515b30aa17a5faa2807",
+			},
+			expURL: "ftp://example.com",
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			assert.Equal(t, test.expURL, InferSubmoduleURL(test.submodule))
+		})
+	}
+}

+ 95 - 0
internal/gitutil/tag.go

@@ -0,0 +1,95 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"github.com/pkg/errors"
+)
+
+// TagsPage contains a list of tags and pagination information.
+type TagsPage struct {
+	// List of tags in the current page.
+	Tags []string
+	// Whether the results include the latest tag.
+	HasLatest bool
+	// When results do not include the latest tag, an indicator of 'after' to go back.
+	PreviousAfter string
+	// Whether there are more tags in the next page.
+	HasNext bool
+}
+
+func (moduler) ListTagsAfter(repoPath, after string, limit int) (*TagsPage, error) {
+	all, err := Module.RepoTags(repoPath)
+	if err != nil {
+		return nil, errors.Wrap(err, "get tags")
+	}
+	total := len(all)
+
+	if limit < 0 {
+		limit = 0
+	}
+
+	// Returns everything when no filter and no limit
+	if after == "" && limit == 0 {
+		return &TagsPage{
+			Tags:      all,
+			HasLatest: true,
+		}, nil
+	}
+
+	// No filter but has a limit, returns first X tags
+	if after == "" && limit > 0 {
+		endIdx := limit
+		if limit > total {
+			endIdx = total
+		}
+		return &TagsPage{
+			Tags:      all[:endIdx],
+			HasLatest: true,
+			HasNext:   limit < total,
+		}, nil
+	}
+
+	// Loop over all tags see if we can find the filter
+	previousAfter := ""
+	found := false
+	tags := make([]string, 0, len(all))
+	for i := range all {
+		if all[i] != after {
+			continue
+		}
+
+		found = true
+		if limit > 0 && i-limit >= 0 {
+			previousAfter = all[i-limit]
+		}
+
+		// In case filter is the oldest one
+		if i+1 < total {
+			tags = all[i+1:]
+		}
+		break
+	}
+
+	if !found {
+		tags = all
+	}
+
+	// If all tags after match is equal to the limit, it reaches the oldest tag as well.
+	if limit == 0 || len(tags) <= limit {
+		return &TagsPage{
+			Tags:          tags,
+			HasLatest:     !found,
+			PreviousAfter: previousAfter,
+		}, nil
+	}
+
+	return &TagsPage{
+		Tags:          tags[:limit],
+		HasLatest:     !found,
+		PreviousAfter: previousAfter,
+		HasNext:       true,
+	}, nil
+}

+ 109 - 0
internal/gitutil/tag_test.go

@@ -0,0 +1,109 @@
+// Copyright 2020 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 gitutil
+
+import (
+	"testing"
+
+	"github.com/gogs/git-module"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestModuler_ListTagsAfter(t *testing.T) {
+	MockModule.RepoTags = func(string, ...git.TagsOptions) ([]string, error) {
+		return []string{
+			"v2.3.0", "v2.2.1", "v2.1.0",
+			"v1.3.0", "v1.2.0", "v1.1.0",
+			"v0.8.0", "v0.5.0", "v0.1.0",
+		}, nil
+	}
+	defer func() {
+		MockModule = MockModuleStore{}
+	}()
+
+	tests := []struct {
+		name        string
+		after       string
+		expTagsPage *TagsPage
+	}{
+		{
+			name: "first page",
+			expTagsPage: &TagsPage{
+				Tags: []string{
+					"v2.3.0", "v2.2.1", "v2.1.0",
+				},
+				HasLatest: true,
+				HasNext:   true,
+			},
+		},
+		{
+			name:  "second page",
+			after: "v2.1.0",
+			expTagsPage: &TagsPage{
+				Tags: []string{
+					"v1.3.0", "v1.2.0", "v1.1.0",
+				},
+				HasLatest: false,
+				HasNext:   true,
+			},
+		},
+		{
+			name:  "last page",
+			after: "v1.1.0",
+			expTagsPage: &TagsPage{
+				Tags: []string{
+					"v0.8.0", "v0.5.0", "v0.1.0",
+				},
+				HasLatest:     false,
+				PreviousAfter: "v2.1.0",
+				HasNext:       false,
+			},
+		},
+
+		{
+			name:  "arbitrary after",
+			after: "v1.2.0",
+			expTagsPage: &TagsPage{
+				Tags: []string{
+					"v1.1.0", "v0.8.0", "v0.5.0",
+				},
+				HasLatest:     false,
+				PreviousAfter: "v2.2.1",
+				HasNext:       true,
+			},
+		},
+		{
+			name:  "after the oldest one",
+			after: "v0.1.0",
+			expTagsPage: &TagsPage{
+				Tags:          []string{},
+				HasLatest:     false,
+				PreviousAfter: "v1.1.0",
+				HasNext:       false,
+			},
+		},
+		{
+			name:  "after does not exist",
+			after: "v2.2.9",
+			expTagsPage: &TagsPage{
+				Tags: []string{
+					"v2.3.0", "v2.2.1", "v2.1.0",
+				},
+				HasLatest: true,
+				HasNext:   true,
+			},
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			tagsPage, err := Module.ListTagsAfter("", test.after, 3)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			assert.Equal(t, test.expTagsPage, tagsPage)
+		})
+	}
+}

+ 1 - 1
internal/route/api/v1/convert/convert.go

@@ -43,7 +43,7 @@ func ToCommit(c *git.Commit) *api.PayloadCommit {
 	}
 	return &api.PayloadCommit{
 		ID:      c.ID.String(),
-		Message: c.Message(),
+		Message: c.Message,
 		URL:     "Not implemented",
 		Author: &api.PayloadUser{
 			Name:     c.Author.Name,

+ 19 - 18
internal/route/api/v1/repo/commits.go

@@ -16,6 +16,7 @@ import (
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/db/errors"
+	"gogs.io/gogs/internal/gitutil"
 )
 
 func GetSingleCommit(c *context.APIContext) {
@@ -25,14 +26,14 @@ func GetSingleCommit(c *context.APIContext) {
 		return
 	}
 
-	gitRepo, err := git.OpenRepository(c.Repo.Repository.RepoPath())
+	gitRepo, err := git.Open(c.Repo.Repository.RepoPath())
 	if err != nil {
-		c.ServerError("OpenRepository", err)
+		c.ServerError("open repository", err)
 		return
 	}
-	commit, err := gitRepo.GetCommit(c.Params(":sha"))
+	commit, err := gitRepo.CatFileCommit(c.Params(":sha"))
 	if err != nil {
-		c.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get commit", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
@@ -59,8 +60,8 @@ func GetSingleCommit(c *context.APIContext) {
 	}
 
 	// Retrieve parent(s) of the commit
-	apiParents := make([]*api.CommitMeta, commit.ParentCount())
-	for i := 0; i < commit.ParentCount(); i++ {
+	apiParents := make([]*api.CommitMeta, commit.ParentsCount())
+	for i := 0; i < commit.ParentsCount(); i++ {
 		sha, _ := commit.ParentID(i)
 		apiParents[i] = &api.CommitMeta{
 			URL: c.BaseURL + "/repos/" + c.Repo.Repository.FullName() + "/commits/" + sha.String(),
@@ -99,24 +100,24 @@ func GetSingleCommit(c *context.APIContext) {
 }
 
 func GetReferenceSHA(c *context.APIContext) {
-	gitRepo, err := git.OpenRepository(c.Repo.Repository.RepoPath())
+	gitRepo, err := git.Open(c.Repo.Repository.RepoPath())
 	if err != nil {
-		c.ServerError("OpenRepository", err)
+		c.ServerError("open repository", err)
 		return
 	}
 
 	ref := c.Params("*")
-	refType := 0 // 0-undetermined, 1-branch, 2-tag
-	if strings.HasPrefix(ref, git.BRANCH_PREFIX) {
-		ref = strings.TrimPrefix(ref, git.BRANCH_PREFIX)
+	refType := 0 // 0-unknown, 1-branch, 2-tag
+	if strings.HasPrefix(ref, git.RefsHeads) {
+		ref = strings.TrimPrefix(ref, git.RefsHeads)
 		refType = 1
-	} else if strings.HasPrefix(ref, git.TAG_PREFIX) {
-		ref = strings.TrimPrefix(ref, git.TAG_PREFIX)
+	} else if strings.HasPrefix(ref, git.RefsTags) {
+		ref = strings.TrimPrefix(ref, git.RefsTags)
 		refType = 2
 	} else {
-		if gitRepo.IsBranchExist(ref) {
+		if gitRepo.HasBranch(ref) {
 			refType = 1
-		} else if gitRepo.IsTagExist(ref) {
+		} else if gitRepo.HasTag(ref) {
 			refType = 2
 		} else {
 			c.NotFound()
@@ -126,12 +127,12 @@ func GetReferenceSHA(c *context.APIContext) {
 
 	var sha string
 	if refType == 1 {
-		sha, err = gitRepo.GetBranchCommitID(ref)
+		sha, err = gitRepo.BranchCommitID(ref)
 	} else if refType == 2 {
-		sha, err = gitRepo.GetTagCommitID(ref)
+		sha, err = gitRepo.TagCommitID(ref)
 	}
 	if err != nil {
-		c.NotFoundOrServerError("get reference commit ID", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get reference commit ID", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 	c.PlainText(http.StatusOK, []byte(sha))

+ 30 - 45
internal/route/api/v1/repo/contents.go

@@ -7,11 +7,9 @@ package repo
 import (
 	"encoding/base64"
 	"fmt"
-	"io/ioutil"
-
-	"github.com/gogs/git-module"
 
 	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/gitutil"
 )
 
 type repoContent struct {
@@ -38,9 +36,9 @@ type Links struct {
 }
 
 func GetContents(c *context.APIContext) {
-	treeEntry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath)
+	treeEntry, err := c.Repo.Commit.TreeEntry(c.Repo.TreePath)
 	if err != nil {
-		c.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get tree entry", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 	username := c.Params(":username")
@@ -56,8 +54,8 @@ func GetContents(c *context.APIContext) {
 	// :baseurl/repos/:username/:project/tree/:sha
 	templateHTMLLLink := "%s/repos/%s/%s/tree/%s"
 
-	gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, treeEntry.ID.String())
-	htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, treeEntry.ID.String())
+	gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, treeEntry.ID().String())
+	htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, treeEntry.ID().String())
 	selfURL := fmt.Sprintf(templateSelfLink, c.BaseURL, username, reponame, c.Repo.TreePath)
 
 	// TODO(unknwon): Make a treeEntryToRepoContent helper.
@@ -65,7 +63,7 @@ func GetContents(c *context.APIContext) {
 		Size:        treeEntry.Size(),
 		Name:        treeEntry.Name(),
 		Path:        c.Repo.TreePath,
-		Sha:         treeEntry.ID.String(),
+		Sha:         treeEntry.ID().String(),
 		URL:         selfURL,
 		GitURL:      gitURL,
 		HTMLURL:     htmlURL,
@@ -82,65 +80,57 @@ func GetContents(c *context.APIContext) {
 	//   2. SubModule
 	//   3. SymLink
 	//   4. Blob (file)
-	if treeEntry.IsSubModule() {
+	if treeEntry.IsCommit() {
 		// TODO(unknwon): submoduleURL is not set as current git-module doesn't handle it properly
 		contents.Type = "submodule"
 		c.JSONSuccess(contents)
 		return
 
-	} else if treeEntry.IsLink() {
+	} else if treeEntry.IsSymlink() {
 		contents.Type = "symlink"
-		blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+		blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
 		if err != nil {
 			c.ServerError("GetBlobByPath", err)
 			return
 		}
-		b, err := blob.Data()
+		p, err := blob.Bytes()
 		if err != nil {
 			c.ServerError("Data", err)
 			return
 		}
-		buf, err := ioutil.ReadAll(b)
-		if err != nil {
-			c.ServerError("ReadAll", err)
-			return
-		}
-		contents.Target = string(buf)
+		contents.Target = string(p)
 		c.JSONSuccess(contents)
 		return
 
-	} else if treeEntry.Type == "blob" {
-		blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+	} else if treeEntry.IsBlob() {
+		blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
 		if err != nil {
 			c.ServerError("GetBlobByPath", err)
 			return
 		}
-		b, err := blob.Data()
+		p, err := blob.Bytes()
 		if err != nil {
 			c.ServerError("Data", err)
 			return
 		}
-		buf, err := ioutil.ReadAll(b)
-		if err != nil {
-			c.ServerError("ReadAll", err)
-			return
-		}
-		contents.Content = base64.StdEncoding.EncodeToString(buf)
+		contents.Content = base64.StdEncoding.EncodeToString(p)
 		contents.Type = "file"
 		c.JSONSuccess(contents)
 		return
 	}
 
+	// TODO: treeEntry.IsExec()
+
 	// treeEntry is a directory
-	dirTree, err := c.Repo.GitRepo.GetTree(treeEntry.ID.String())
+	dirTree, err := c.Repo.GitRepo.LsTree(treeEntry.ID().String())
 	if err != nil {
-		c.NotFoundOrServerError("GetTree", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get tree", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
-	entries, err := dirTree.ListEntries()
+	entries, err := dirTree.Entries()
 	if err != nil {
-		c.NotFoundOrServerError("ListEntries", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("list entries", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
@@ -151,33 +141,28 @@ func GetContents(c *context.APIContext) {
 
 	var results = make([]*repoContent, 0, len(entries))
 	for _, entry := range entries {
-		gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, entry.ID.String())
-		htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, entry.ID.String())
+		gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, entry.ID().String())
+		htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, entry.ID().String())
 		selfURL := fmt.Sprintf(templateSelfLink, c.BaseURL, username, reponame, c.Repo.TreePath)
 		var contentType string
-		if entry.IsDir() {
+		if entry.IsTree() {
 			contentType = "dir"
-		} else if entry.IsSubModule() {
+		} else if entry.IsCommit() {
 			// TODO(unknwon): submoduleURL is not set as current git-module doesn't handle it properly
 			contentType = "submodule"
-		} else if entry.IsLink() {
+		} else if entry.IsSymlink() {
 			contentType = "symlink"
-			blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+			blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
 			if err != nil {
 				c.ServerError("GetBlobByPath", err)
 				return
 			}
-			b, err := blob.Data()
+			p, err := blob.Bytes()
 			if err != nil {
 				c.ServerError("Data", err)
 				return
 			}
-			buf, err := ioutil.ReadAll(b)
-			if err != nil {
-				c.ServerError("ReadAll", err)
-				return
-			}
-			contents.Target = string(buf)
+			contents.Target = string(p)
 		} else {
 			contentType = "file"
 		}
@@ -187,7 +172,7 @@ func GetContents(c *context.APIContext) {
 			Size:        entry.Size(),
 			Name:        entry.Name(),
 			Path:        c.Repo.TreePath,
-			Sha:         entry.ID.String(),
+			Sha:         entry.ID().String(),
 			URL:         selfURL,
 			GitURL:      gitURL,
 			HTMLURL:     htmlURL,

+ 8 - 7
internal/route/api/v1/repo/file.go

@@ -9,6 +9,7 @@ import (
 
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/route/repo"
 )
 
@@ -23,9 +24,9 @@ func GetRawFile(c *context.APIContext) {
 		return
 	}
 
-	blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+	blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
 	if err != nil {
-		c.NotFoundOrServerError("GetBlobByPath", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get blob", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 	if err = repo.ServeBlob(c.Context, blob); err != nil {
@@ -35,9 +36,9 @@ func GetRawFile(c *context.APIContext) {
 
 func GetArchive(c *context.APIContext) {
 	repoPath := db.RepoPath(c.Params(":username"), c.Params(":reponame"))
-	gitRepo, err := git.OpenRepository(repoPath)
+	gitRepo, err := git.Open(repoPath)
 	if err != nil {
-		c.ServerError("OpenRepository", err)
+		c.ServerError("open repository", err)
 		return
 	}
 	c.Repo.GitRepo = gitRepo
@@ -46,16 +47,16 @@ func GetArchive(c *context.APIContext) {
 }
 
 func GetEditorconfig(c *context.APIContext) {
-	ec, err := c.Repo.GetEditorconfig()
+	ec, err := c.Repo.Editorconfig()
 	if err != nil {
-		c.NotFoundOrServerError("GetEditorconfig", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get .editorconfig", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
 	fileName := c.Params("filename")
 	def, err := ec.GetDefinitionForFilename(fileName)
 	if err != nil {
-		c.ServerError("GetDefinitionForFilename", err)
+		c.ServerError("get definition for filename", err)
 		return
 	}
 	if def == nil {

+ 13 - 8
internal/route/api/v1/repo/tree.go

@@ -1,3 +1,7 @@
+// Copyright 2020 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 repo
 
 import (
@@ -6,6 +10,7 @@ import (
 	"github.com/gogs/git-module"
 
 	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/gitutil"
 )
 
 type repoGitTree struct {
@@ -24,14 +29,14 @@ type repoGitTreeEntry struct {
 }
 
 func GetRepoGitTree(c *context.APIContext) {
-	gitTree, err := c.Repo.GitRepo.GetTree(c.Params(":sha"))
+	gitTree, err := c.Repo.GitRepo.LsTree(c.Params(":sha"))
 	if err != nil {
-		c.NotFoundOrServerError("GetRepoGitTree", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get tree", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
-	entries, err := gitTree.ListEntries()
+	entries, err := gitTree.Entries()
 	if err != nil {
-		c.ServerError("GetRepoGitTree", err)
+		c.ServerError("list entries", err)
 		return
 	}
 
@@ -48,7 +53,7 @@ func GetRepoGitTree(c *context.APIContext) {
 	children := make([]*repoGitTreeEntry, 0, len(entries))
 	for _, entry := range entries {
 		var mode string
-		switch entry.Type {
+		switch entry.Type() {
 		case git.ObjectCommit:
 			mode = "160000"
 		case git.ObjectTree:
@@ -63,10 +68,10 @@ func GetRepoGitTree(c *context.APIContext) {
 		children = append(children, &repoGitTreeEntry{
 			Path: entry.Name(),
 			Mode: mode,
-			Type: string(entry.Type),
+			Type: string(entry.Type()),
 			Size: entry.Size(),
-			Sha:  entry.ID.String(),
-			URL:  fmt.Sprintf(templateURL+"/%s", entry.ID.String()),
+			Sha:  entry.ID().String(),
+			URL:  fmt.Sprintf(templateURL+"/%s", entry.ID().String()),
 		})
 	}
 	c.JSONSuccess(&repoGitTree{

+ 2 - 2
internal/route/install.go

@@ -41,9 +41,9 @@ func checkRunMode() {
 	if conf.IsProdMode() {
 		macaron.Env = macaron.PROD
 		macaron.ColorLog = false
-		git.Debug = false
+		git.SetOutput(nil)
 	} else {
-		git.Debug = true
+		git.SetOutput(os.Stdout)
 	}
 	log.Info("Run mode: %s", strings.Title(macaron.Env))
 }

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

@@ -119,11 +119,11 @@ func DeleteBranchPost(c *context.Context) {
 		c.Redirect(redirectTo)
 	}()
 
-	if !c.Repo.GitRepo.IsBranchExist(branchName) {
+	if !c.Repo.GitRepo.HasBranch(branchName) {
 		return
 	}
 	if len(commitID) > 0 {
-		branchCommitID, err := c.Repo.GitRepo.GetBranchCommitID(branchName)
+		branchCommitID, err := c.Repo.GitRepo.BranchCommitID(branchName)
 		if err != nil {
 			log.Error("Failed to get commit ID of branch %q: %v", branchName, err)
 			return

+ 36 - 47
internal/route/repo/commit.go

@@ -5,7 +5,6 @@
 package repo
 
 import (
-	"container/list"
 	"path"
 
 	"github.com/gogs/git-module"
@@ -13,6 +12,7 @@ import (
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/tool"
 )
 
@@ -33,13 +33,9 @@ func RefCommits(c *context.Context) {
 	}
 }
 
-func RenderIssueLinks(oldCommits *list.List, repoLink string) *list.List {
-	newCommits := list.New()
-	for e := oldCommits.Front(); e != nil; e = e.Next() {
-		c := e.Value.(*git.Commit)
-		newCommits.PushBack(c)
-	}
-	return newCommits
+// TODO(unknwon)
+func RenderIssueLinks(oldCommits []*git.Commit, repoLink string) []*git.Commit {
+	return oldCommits
 }
 
 func renderCommits(c *context.Context, filename string) {
@@ -53,30 +49,23 @@ func renderCommits(c *context.Context, filename string) {
 	}
 	pageSize := c.QueryInt("pageSize")
 	if pageSize < 1 {
-		pageSize = git.DefaultCommitsPageSize
+		pageSize = conf.UI.User.CommitsPagingNum
 	}
 
-	// Both 'git log branchName' and 'git log commitID' work.
-	var err error
-	var commits *list.List
-	if len(filename) == 0 {
-		commits, err = c.Repo.Commit.CommitsByRangeSize(page, pageSize)
-	} else {
-		commits, err = c.Repo.GitRepo.CommitsByFileAndRangeSize(c.Repo.BranchName, filename, page, pageSize)
-	}
+	commits, err := c.Repo.Commit.CommitsByPage(page, pageSize, git.CommitsByPageOptions{Path: filename})
 	if err != nil {
-		c.Handle(500, "CommitsByRangeSize/CommitsByFileAndRangeSize", err)
+		c.ServerError("paging commits", err)
 		return
 	}
+
 	commits = RenderIssueLinks(commits, c.Repo.RepoLink)
-	commits = db.ValidateCommitsWithEmails(commits)
-	c.Data["Commits"] = commits
+	c.Data["Commits"] = db.ValidateCommitsWithEmails(commits)
 
 	if page > 1 {
 		c.Data["HasPrevious"] = true
 		c.Data["PreviousPage"] = page - 1
 	}
-	if commits.Len() == pageSize {
+	if len(commits) == pageSize {
 		c.Data["HasNext"] = true
 		c.Data["NextPage"] = page + 1
 	}
@@ -102,12 +91,12 @@ func SearchCommits(c *context.Context) {
 
 	commits, err := c.Repo.Commit.SearchCommits(keyword)
 	if err != nil {
-		c.Handle(500, "SearchCommits", err)
+		c.ServerError("SearchCommits", err)
 		return
 	}
+
 	commits = RenderIssueLinks(commits, c.Repo.RepoLink)
-	commits = db.ValidateCommitsWithEmails(commits)
-	c.Data["Commits"] = commits
+	c.Data["Commits"] = db.ValidateCommitsWithEmails(commits)
 
 	c.Data["Keyword"] = keyword
 	c.Data["Username"] = c.Repo.Owner.Name
@@ -128,22 +117,23 @@ func Diff(c *context.Context) {
 	repoName := c.Repo.Repository.Name
 	commitID := c.Params(":sha")
 
-	commit, err := c.Repo.GitRepo.GetCommit(commitID)
+	commit, err := c.Repo.GitRepo.CatFileCommit(commitID)
 	if err != nil {
-		c.NotFoundOrServerError("get commit by ID", git.IsErrNotExist, err)
+		// TODO: Move checker to gitutil package
+		c.NotFoundOrServerError("get commit by ID", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
-	diff, err := db.GetDiffCommit(db.RepoPath(userName, repoName),
-		commitID, conf.Git.MaxGitDiffLines,
-		conf.Git.MaxGitDiffLineCharacters, conf.Git.MaxGitDiffFiles)
+	diff, err := gitutil.RepoDiff(c.Repo.GitRepo,
+		commitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
+	)
 	if err != nil {
-		c.NotFoundOrServerError("get diff commit", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get diff", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
-	parents := make([]string, commit.ParentCount())
-	for i := 0; i < commit.ParentCount(); i++ {
+	parents := make([]string, commit.ParentsCount())
+	for i := 0; i < commit.ParentsCount(); i++ {
 		sha, err := commit.ParentID(i)
 		parents[i] = sha.String()
 		if err != nil {
@@ -169,7 +159,7 @@ func Diff(c *context.Context) {
 	c.Data["Parents"] = parents
 	c.Data["DiffNotAvailable"] = diff.NumFiles() == 0
 	c.Data["SourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", commitID)
-	if commit.ParentCount() > 0 {
+	if commit.ParentsCount() > 0 {
 		c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", parents[0])
 	}
 	c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "raw", commitID)
@@ -177,13 +167,12 @@ func Diff(c *context.Context) {
 }
 
 func RawDiff(c *context.Context) {
-	if err := git.GetRawDiff(
-		db.RepoPath(c.Repo.Owner.Name, c.Repo.Repository.Name),
+	if err := c.Repo.GitRepo.RawDiff(
 		c.Params(":sha"),
-		git.RawDiffType(c.Params(":ext")),
+		git.RawDiffFormat(c.Params(":ext")),
 		c.Resp,
 	); err != nil {
-		c.NotFoundOrServerError("GetRawDiff", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get raw diff", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 }
@@ -195,31 +184,31 @@ func CompareDiff(c *context.Context) {
 	beforeCommitID := c.Params(":before")
 	afterCommitID := c.Params(":after")
 
-	commit, err := c.Repo.GitRepo.GetCommit(afterCommitID)
+	commit, err := c.Repo.GitRepo.CatFileCommit(afterCommitID)
 	if err != nil {
 		c.Handle(404, "GetCommit", err)
 		return
 	}
 
-	diff, err := db.GetDiffRange(db.RepoPath(userName, repoName), beforeCommitID,
-		afterCommitID, conf.Git.MaxGitDiffLines,
-		conf.Git.MaxGitDiffLineCharacters, conf.Git.MaxGitDiffFiles)
+	diff, err := gitutil.RepoDiff(c.Repo.GitRepo,
+		afterCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
+		git.DiffOptions{Base: beforeCommitID},
+	)
 	if err != nil {
-		c.Handle(404, "GetDiffRange", err)
+		c.ServerError("get diff", err)
 		return
 	}
 
-	commits, err := commit.CommitsBeforeUntil(beforeCommitID)
+	commits, err := commit.CommitsAfter(beforeCommitID)
 	if err != nil {
-		c.Handle(500, "CommitsBeforeUntil", err)
+		c.ServerError("get commits after", err)
 		return
 	}
-	commits = db.ValidateCommitsWithEmails(commits)
 
 	c.Data["IsSplitStyle"] = c.Query("style") == "split"
 	c.Data["CommitRepoLink"] = c.Repo.RepoLink
-	c.Data["Commits"] = commits
-	c.Data["CommitsCount"] = commits.Len()
+	c.Data["Commits"] = db.ValidateCommitsWithEmails(commits)
+	c.Data["CommitsCount"] = len(commits)
 	c.Data["BeforeCommitID"] = beforeCommitID
 	c.Data["AfterCommitID"] = afterCommitID
 	c.Data["Username"] = userName

+ 16 - 25
internal/route/repo/download.go

@@ -6,32 +6,26 @@ package repo
 
 import (
 	"fmt"
-	"io"
 	"net/http"
 	"path"
 
 	"github.com/gogs/git-module"
 
-	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/conf"
+	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/tool"
 )
 
-func serveData(c *context.Context, name string, r io.Reader) error {
-	buf := make([]byte, 1024)
-	n, _ := r.Read(buf)
-	if n >= 0 {
-		buf = buf[:n]
-	}
-
-	commit, err := c.Repo.Commit.GetCommitByPath(c.Repo.TreePath)
+func serveData(c *context.Context, name string, data []byte) error {
+	commit, err := c.Repo.Commit.CommitByPath(git.CommitByRevisionOptions{Path: c.Repo.TreePath})
 	if err != nil {
-		return fmt.Errorf("GetCommitByPath: %v", err)
+		return fmt.Errorf("get commit by path %q: %v", c.Repo.TreePath, err)
 	}
 	c.Resp.Header().Set("Last-Modified", commit.Committer.When.Format(http.TimeFormat))
 
-	if !tool.IsTextFile(buf) {
-		if !tool.IsImageFile(buf) {
+	if !tool.IsTextFile(data) {
+		if !tool.IsImageFile(data) {
 			c.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+name+"\"")
 			c.Resp.Header().Set("Content-Transfer-Encoding", "binary")
 		}
@@ -39,33 +33,30 @@ func serveData(c *context.Context, name string, r io.Reader) error {
 		c.Resp.Header().Set("Content-Type", "text/plain; charset=utf-8")
 	}
 
-	if _, err := c.Resp.Write(buf); err != nil {
+	if _, err := c.Resp.Write(data); err != nil {
 		return fmt.Errorf("write buffer to response: %v", err)
 	}
-	_, err = io.Copy(c.Resp, r)
-	return err
+	return nil
 }
 
 func ServeBlob(c *context.Context, blob *git.Blob) error {
-	dataRc, err := blob.Data()
+	p, err := blob.Bytes()
 	if err != nil {
 		return err
 	}
 
-	return serveData(c, path.Base(c.Repo.TreePath), dataRc)
+	return serveData(c, path.Base(c.Repo.TreePath), p)
 }
 
 func SingleDownload(c *context.Context) {
-	blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+	blob, err := c.Repo.Commit.Blob(c.Repo.TreePath)
 	if err != nil {
-		if git.IsErrNotExist(err) {
-			c.Handle(404, "GetBlobByPath", nil)
-		} else {
-			c.Handle(500, "GetBlobByPath", err)
-		}
+		c.NotFoundOrServerError("get blob", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
+
 	if err = ServeBlob(c, blob); err != nil {
-		c.Handle(500, "ServeBlob", err)
+		c.ServerError("serve blob", err)
+		return
 	}
 }

+ 23 - 30
internal/route/repo/editor.go

@@ -6,19 +6,18 @@ package repo
 
 import (
 	"fmt"
-	"io/ioutil"
 	"net/http"
 	"path"
 	"strings"
 
 	log "unknwon.dev/clog/v2"
 
-	"github.com/gogs/git-module"
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/form"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/pathutil"
 	"gogs.io/gogs/internal/template"
 	"gogs.io/gogs/internal/tool"
@@ -55,20 +54,20 @@ func editFile(c *context.Context, isNewFile bool) {
 	treeNames, treePaths := getParentTreeFields(c.Repo.TreePath)
 
 	if !isNewFile {
-		entry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath)
+		entry, err := c.Repo.Commit.TreeEntry(c.Repo.TreePath)
 		if err != nil {
-			c.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
+			c.NotFoundOrServerError("get tree entry", gitutil.IsErrRevisionNotExist, err)
 			return
 		}
 
 		// No way to edit a directory online.
-		if entry.IsDir() {
+		if entry.IsTree() {
 			c.NotFound()
 			return
 		}
 
 		blob := entry.Blob()
-		dataRc, err := blob.Data()
+		p, err := blob.Bytes()
 		if err != nil {
 			c.ServerError("blob.Data", err)
 			return
@@ -77,23 +76,17 @@ func editFile(c *context.Context, isNewFile bool) {
 		c.Data["FileSize"] = blob.Size()
 		c.Data["FileName"] = blob.Name()
 
-		buf := make([]byte, 1024)
-		n, _ := dataRc.Read(buf)
-		buf = buf[:n]
-
 		// Only text file are editable online.
-		if !tool.IsTextFile(buf) {
+		if !tool.IsTextFile(p) {
 			c.NotFound()
 			return
 		}
 
-		d, _ := ioutil.ReadAll(dataRc)
-		buf = append(buf, d...)
-		if err, content := template.ToUTF8WithErr(buf); err != nil {
+		if err, content := template.ToUTF8WithErr(p); err != nil {
 			if err != nil {
 				log.Error("Failed to convert encoding to UTF-8: %v", err)
 			}
-			c.Data["FileContent"] = string(buf)
+			c.Data["FileContent"] = string(p)
 		} else {
 			c.Data["FileContent"] = content
 		}
@@ -182,9 +175,9 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
 	var newTreePath string
 	for index, part := range treeNames {
 		newTreePath = path.Join(newTreePath, part)
-		entry, err := c.Repo.Commit.GetTreeEntryByPath(newTreePath)
+		entry, err := c.Repo.Commit.TreeEntry(newTreePath)
 		if err != nil {
-			if git.IsErrNotExist(err) {
+			if gitutil.IsErrRevisionNotExist(err) {
 				// Means there is no item with that name, so we're good
 				break
 			}
@@ -193,17 +186,17 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
 			return
 		}
 		if index != len(treeNames)-1 {
-			if !entry.IsDir() {
+			if !entry.IsTree() {
 				c.FormErr("TreePath")
 				c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), EDIT_FILE, &f)
 				return
 			}
 		} else {
-			if entry.IsLink() {
+			if entry.IsSymlink() {
 				c.FormErr("TreePath")
 				c.RenderWithErr(c.Tr("repo.editor.file_is_a_symlink", part), EDIT_FILE, &f)
 				return
-			} else if entry.IsDir() {
+			} else if entry.IsTree() {
 				c.FormErr("TreePath")
 				c.RenderWithErr(c.Tr("repo.editor.filename_is_a_directory", part), EDIT_FILE, &f)
 				return
@@ -212,9 +205,9 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
 	}
 
 	if !isNewFile {
-		_, err := c.Repo.Commit.GetTreeEntryByPath(oldTreePath)
+		_, err := c.Repo.Commit.TreeEntry(oldTreePath)
 		if err != nil {
-			if git.IsErrNotExist(err) {
+			if gitutil.IsErrRevisionNotExist(err) {
 				c.FormErr("TreePath")
 				c.RenderWithErr(c.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), EDIT_FILE, &f)
 			} else {
@@ -223,7 +216,7 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
 			return
 		}
 		if lastCommit != c.Repo.CommitID {
-			files, err := c.Repo.Commit.GetFilesChangedSinceCommit(lastCommit)
+			files, err := c.Repo.Commit.FilesChangedAfter(lastCommit)
 			if err != nil {
 				c.ServerError("GetFilesChangedSinceCommit", err)
 				return
@@ -240,9 +233,9 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
 
 	if oldTreePath != f.TreePath {
 		// We have a new filename (rename or completely new file) so we need to make sure it doesn't already exist, can't clobber.
-		entry, err := c.Repo.Commit.GetTreeEntryByPath(f.TreePath)
+		entry, err := c.Repo.Commit.TreeEntry(f.TreePath)
 		if err != nil {
-			if !git.IsErrNotExist(err) {
+			if !gitutil.IsErrRevisionNotExist(err) {
 				c.ServerError("GetTreeEntryByPath", err)
 				return
 			}
@@ -302,11 +295,11 @@ func NewFilePost(c *context.Context, f form.EditRepoFile) {
 func DiffPreviewPost(c *context.Context, f form.EditPreviewDiff) {
 	treePath := c.Repo.TreePath
 
-	entry, err := c.Repo.Commit.GetTreeEntryByPath(treePath)
+	entry, err := c.Repo.Commit.TreeEntry(treePath)
 	if err != nil {
 		c.Error(500, "GetTreeEntryByPath: "+err.Error())
 		return
-	} else if entry.IsDir() {
+	} else if entry.IsTree() {
 		c.Error(422)
 		return
 	}
@@ -468,9 +461,9 @@ func UploadFilePost(c *context.Context, f form.UploadRepoFile) {
 	var newTreePath string
 	for _, part := range treeNames {
 		newTreePath = path.Join(newTreePath, part)
-		entry, err := c.Repo.Commit.GetTreeEntryByPath(newTreePath)
+		entry, err := c.Repo.Commit.TreeEntry(newTreePath)
 		if err != nil {
-			if git.IsErrNotExist(err) {
+			if gitutil.IsErrRevisionNotExist(err) {
 				// Means there is no item with that name, so we're good
 				break
 			}
@@ -480,7 +473,7 @@ func UploadFilePost(c *context.Context, f form.UploadRepoFile) {
 		}
 
 		// User can only upload files to a directory.
-		if !entry.IsDir() {
+		if !entry.IsTree() {
 			c.FormErr("TreePath")
 			c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), UPLOAD_FILE, &f)
 			return

+ 5 - 14
internal/route/repo/issue.go

@@ -6,8 +6,6 @@ package repo
 
 import (
 	"fmt"
-	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -303,30 +301,23 @@ func RetrieveRepoMetas(c *context.Context, repo *db.Repository) []*db.Label {
 }
 
 func getFileContentFromDefaultBranch(c *context.Context, filename string) (string, bool) {
-	var r io.Reader
-	var bytes []byte
-
 	if c.Repo.Commit == nil {
 		var err error
-		c.Repo.Commit, err = c.Repo.GitRepo.GetBranchCommit(c.Repo.Repository.DefaultBranch)
+		c.Repo.Commit, err = c.Repo.GitRepo.BranchCommit(c.Repo.Repository.DefaultBranch)
 		if err != nil {
 			return "", false
 		}
 	}
 
-	entry, err := c.Repo.Commit.GetTreeEntryByPath(filename)
-	if err != nil {
-		return "", false
-	}
-	r, err = entry.Blob().Data()
+	entry, err := c.Repo.Commit.TreeEntry(filename)
 	if err != nil {
 		return "", false
 	}
-	bytes, err = ioutil.ReadAll(r)
+	p, err := entry.Blob().Bytes()
 	if err != nil {
 		return "", false
 	}
-	return string(bytes), true
+	return string(p), true
 }
 
 func setTemplateIfExists(c *context.Context, ctxDataKey string, possibleFiles []string) {
@@ -656,7 +647,7 @@ func viewIssue(c *context.Context, isPullList bool) {
 		}
 
 		c.Data["IsPullBranchDeletable"] = pull.BaseRepoID == pull.HeadRepoID &&
-			c.Repo.IsWriter() && c.Repo.GitRepo.IsBranchExist(pull.HeadBranch) &&
+			c.Repo.IsWriter() && c.Repo.GitRepo.HasBranch(pull.HeadBranch) &&
 			!branchProtected
 
 		c.Data["DeleteBranchLink"] = c.Repo.MakeURL(url.URL{

+ 72 - 68
internal/route/repo/pull.go

@@ -5,7 +5,6 @@
 package repo
 
 import (
-	"container/list"
 	"path"
 	"strings"
 
@@ -19,6 +18,7 @@ import (
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/form"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/tool"
 )
 
@@ -183,19 +183,21 @@ func PrepareMergedViewPullInfo(c *context.Context, issue *db.Issue) {
 	c.Data["BaseTarget"] = c.Repo.Owner.Name + "/" + pull.BaseBranch
 
 	var err error
-	c.Data["NumCommits"], err = c.Repo.GitRepo.CommitsCountBetween(pull.MergeBase, pull.MergedCommitID)
+	c.Data["NumCommits"], err = c.Repo.GitRepo.RevListCount([]string{pull.MergeBase + "..." + pull.MergedCommitID})
 	if err != nil {
 		c.ServerError("Repo.GitRepo.CommitsCountBetween", err)
 		return
 	}
-	c.Data["NumFiles"], err = c.Repo.GitRepo.FilesCountBetween(pull.MergeBase, pull.MergedCommitID)
+
+	names, err := c.Repo.GitRepo.DiffNameOnly(pull.MergeBase, pull.MergedCommitID, git.DiffNameOnlyOptions{NeedsMergeBase: true})
+	c.Data["NumFiles"] = len(names)
 	if err != nil {
 		c.ServerError("Repo.GitRepo.FilesCountBetween", err)
 		return
 	}
 }
 
-func PrepareViewPullInfo(c *context.Context, issue *db.Issue) *git.PullRequestInfo {
+func PrepareViewPullInfo(c *context.Context, issue *db.Issue) *gitutil.PullRequestMeta {
 	repo := c.Repo.Repository
 	pull := issue.PullRequest
 
@@ -208,14 +210,14 @@ func PrepareViewPullInfo(c *context.Context, issue *db.Issue) *git.PullRequestIn
 	)
 
 	if pull.HeadRepo != nil {
-		headGitRepo, err = git.OpenRepository(pull.HeadRepo.RepoPath())
+		headGitRepo, err = git.Open(pull.HeadRepo.RepoPath())
 		if err != nil {
-			c.ServerError("OpenRepository", err)
+			c.ServerError("open repository", err)
 			return nil
 		}
 	}
 
-	if pull.HeadRepo == nil || !headGitRepo.IsBranchExist(pull.HeadBranch) {
+	if pull.HeadRepo == nil || !headGitRepo.HasBranch(pull.HeadBranch) {
 		c.Data["IsPullReuqestBroken"] = true
 		c.Data["HeadTarget"] = "deleted"
 		c.Data["NumCommits"] = 0
@@ -223,8 +225,8 @@ func PrepareViewPullInfo(c *context.Context, issue *db.Issue) *git.PullRequestIn
 		return nil
 	}
 
-	prInfo, err := headGitRepo.GetPullRequestInfo(db.RepoPath(repo.Owner.Name, repo.Name),
-		pull.BaseBranch, pull.HeadBranch)
+	baseRepoPath := db.RepoPath(repo.Owner.Name, repo.Name)
+	prMeta, err := gitutil.Module.PullRequestMeta(headGitRepo.Path(), baseRepoPath, pull.HeadBranch, pull.BaseBranch)
 	if err != nil {
 		if strings.Contains(err.Error(), "fatal: Not a valid object name") {
 			c.Data["IsPullReuqestBroken"] = true
@@ -237,9 +239,9 @@ func PrepareViewPullInfo(c *context.Context, issue *db.Issue) *git.PullRequestIn
 		c.ServerError("GetPullRequestInfo", err)
 		return nil
 	}
-	c.Data["NumCommits"] = prInfo.Commits.Len()
-	c.Data["NumFiles"] = prInfo.NumFiles
-	return prInfo
+	c.Data["NumCommits"] = len(prMeta.Commits)
+	c.Data["NumFiles"] = prMeta.NumFiles
+	return prMeta
 }
 
 func ViewPullCommits(c *context.Context) {
@@ -257,25 +259,25 @@ func ViewPullCommits(c *context.Context) {
 		c.Data["Reponame"] = pull.HeadRepo.Name
 	}
 
-	var commits *list.List
+	var commits []*git.Commit
 	if pull.HasMerged {
 		PrepareMergedViewPullInfo(c, issue)
 		if c.Written() {
 			return
 		}
-		startCommit, err := c.Repo.GitRepo.GetCommit(pull.MergeBase)
+		startCommit, err := c.Repo.GitRepo.CatFileCommit(pull.MergeBase)
 		if err != nil {
-			c.ServerError("Repo.GitRepo.GetCommit", err)
+			c.ServerError("get commit of merge base", err)
 			return
 		}
-		endCommit, err := c.Repo.GitRepo.GetCommit(pull.MergedCommitID)
+		endCommit, err := c.Repo.GitRepo.CatFileCommit(pull.MergedCommitID)
 		if err != nil {
-			c.ServerError("Repo.GitRepo.GetCommit", err)
+			c.ServerError("get merged commit", err)
 			return
 		}
-		commits, err = c.Repo.GitRepo.CommitsBetween(endCommit, startCommit)
+		commits, err = c.Repo.GitRepo.RevList([]string{startCommit.ID.String() + "..." + endCommit.ID.String()})
 		if err != nil {
-			c.ServerError("Repo.GitRepo.CommitsBetween", err)
+			c.ServerError("list commits", err)
 			return
 		}
 
@@ -290,9 +292,8 @@ func ViewPullCommits(c *context.Context) {
 		commits = prInfo.Commits
 	}
 
-	commits = db.ValidateCommitsWithEmails(commits)
-	c.Data["Commits"] = commits
-	c.Data["CommitsCount"] = commits.Len()
+	c.Data["Commits"] = db.ValidateCommitsWithEmails(commits)
+	c.Data["CommitsCount"] = len(commits)
 
 	c.Success(PULL_COMMITS)
 }
@@ -308,7 +309,7 @@ func ViewPullFiles(c *context.Context) {
 	pull := issue.PullRequest
 
 	var (
-		diffRepoPath  string
+		diffGitRepo   *git.Repository
 		startCommitID string
 		endCommitID   string
 		gitRepo       *git.Repository
@@ -320,7 +321,7 @@ func ViewPullFiles(c *context.Context) {
 			return
 		}
 
-		diffRepoPath = c.Repo.GitRepo.Path
+		diffGitRepo = c.Repo.GitRepo
 		startCommitID = pull.MergeBase
 		endCommitID = pull.MergedCommitID
 		gitRepo = c.Repo.GitRepo
@@ -335,37 +336,38 @@ func ViewPullFiles(c *context.Context) {
 
 		headRepoPath := db.RepoPath(pull.HeadUserName, pull.HeadRepo.Name)
 
-		headGitRepo, err := git.OpenRepository(headRepoPath)
+		headGitRepo, err := git.Open(headRepoPath)
 		if err != nil {
-			c.ServerError("OpenRepository", err)
+			c.ServerError("open repository", err)
 			return
 		}
 
-		headCommitID, err := headGitRepo.GetBranchCommitID(pull.HeadBranch)
+		headCommitID, err := headGitRepo.BranchCommitID(pull.HeadBranch)
 		if err != nil {
-			c.ServerError("GetBranchCommitID", err)
+			c.ServerError("get head branch commit ID", err)
 			return
 		}
 
-		diffRepoPath = headRepoPath
+		diffGitRepo = headGitRepo
 		startCommitID = prInfo.MergeBase
 		endCommitID = headCommitID
 		gitRepo = headGitRepo
 	}
 
-	diff, err := db.GetDiffRange(diffRepoPath,
-		startCommitID, endCommitID, conf.Git.MaxGitDiffLines,
-		conf.Git.MaxGitDiffLineCharacters, conf.Git.MaxGitDiffFiles)
+	diff, err := gitutil.RepoDiff(diffGitRepo,
+		endCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
+		git.DiffOptions{Base: startCommitID},
+	)
 	if err != nil {
-		c.ServerError("GetDiffRange", err)
+		c.ServerError("get diff", err)
 		return
 	}
 	c.Data["Diff"] = diff
 	c.Data["DiffNotAvailable"] = diff.NumFiles() == 0
 
-	commit, err := gitRepo.GetCommit(endCommitID)
+	commit, err := gitRepo.CatFileCommit(endCommitID)
 	if err != nil {
-		c.ServerError("GetCommit", err)
+		c.ServerError("get commit", err)
 		return
 	}
 
@@ -424,7 +426,7 @@ func MergePullRequest(c *context.Context) {
 	c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index))
 }
 
-func ParseCompareInfo(c *context.Context) (*db.User, *db.Repository, *git.Repository, *git.PullRequestInfo, string, string) {
+func ParseCompareInfo(c *context.Context) (*db.User, *db.Repository, *git.Repository, *gitutil.PullRequestMeta, string, string) {
 	baseRepo := c.Repo.Repository
 
 	// Get compared branches information
@@ -473,7 +475,7 @@ func ParseCompareInfo(c *context.Context) (*db.User, *db.Repository, *git.Reposi
 	c.Repo.PullRequest.SameRepo = isSameRepo
 
 	// Check if base branch is valid.
-	if !c.Repo.GitRepo.IsBranchExist(baseBranch) {
+	if !c.Repo.GitRepo.HasBranch(baseBranch) {
 		c.NotFound()
 		return nil, nil, nil, nil, "", ""
 	}
@@ -497,9 +499,9 @@ func ParseCompareInfo(c *context.Context) (*db.User, *db.Repository, *git.Reposi
 			return nil, nil, nil, nil, "", ""
 		}
 
-		headGitRepo, err = git.OpenRepository(db.RepoPath(headUser.Name, headRepo.Name))
+		headGitRepo, err = git.Open(db.RepoPath(headUser.Name, headRepo.Name))
 		if err != nil {
-			c.ServerError("OpenRepository", err)
+			c.ServerError("open repository", err)
 			return nil, nil, nil, nil, "", ""
 		}
 	} else {
@@ -514,31 +516,32 @@ func ParseCompareInfo(c *context.Context) (*db.User, *db.Repository, *git.Reposi
 	}
 
 	// Check if head branch is valid.
-	if !headGitRepo.IsBranchExist(headBranch) {
+	if !headGitRepo.HasBranch(headBranch) {
 		c.NotFound()
 		return nil, nil, nil, nil, "", ""
 	}
 
-	headBranches, err := headGitRepo.GetBranches()
+	headBranches, err := headGitRepo.Branches()
 	if err != nil {
-		c.ServerError("GetBranches", err)
+		c.ServerError("get branches", err)
 		return nil, nil, nil, nil, "", ""
 	}
 	c.Data["HeadBranches"] = headBranches
 
-	prInfo, err := headGitRepo.GetPullRequestInfo(db.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch)
+	baseRepoPath := db.RepoPath(baseRepo.Owner.Name, baseRepo.Name)
+	meta, err := gitutil.Module.PullRequestMeta(headGitRepo.Path(), baseRepoPath, headBranch, baseBranch)
 	if err != nil {
-		if git.IsErrNoMergeBase(err) {
+		if gitutil.IsErrNoMergeBase(err) {
 			c.Data["IsNoMergeBase"] = true
 			c.Success(COMPARE_PULL)
 		} else {
-			c.ServerError("GetPullRequestInfo", err)
+			c.ServerError("get pull request meta", err)
 		}
 		return nil, nil, nil, nil, "", ""
 	}
-	c.Data["BeforeCommitID"] = prInfo.MergeBase
+	c.Data["BeforeCommitID"] = meta.MergeBase
 
-	return headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch
+	return headUser, headRepo, headGitRepo, meta, baseBranch, headBranch
 }
 
 func PrepareCompareDiff(
@@ -546,8 +549,9 @@ func PrepareCompareDiff(
 	headUser *db.User,
 	headRepo *db.Repository,
 	headGitRepo *git.Repository,
-	prInfo *git.PullRequestInfo,
-	baseBranch, headBranch string) bool {
+	meta *gitutil.PullRequestMeta,
+	headBranch string,
+) bool {
 
 	var (
 		repo = c.Repo.Repository
@@ -557,44 +561,44 @@ func PrepareCompareDiff(
 	// Get diff information.
 	c.Data["CommitRepoLink"] = headRepo.Link()
 
-	headCommitID, err := headGitRepo.GetBranchCommitID(headBranch)
+	headCommitID, err := headGitRepo.BranchCommitID(headBranch)
 	if err != nil {
-		c.ServerError("GetBranchCommitID", err)
+		c.ServerError("get head branch commit ID", err)
 		return false
 	}
 	c.Data["AfterCommitID"] = headCommitID
 
-	if headCommitID == prInfo.MergeBase {
+	if headCommitID == meta.MergeBase {
 		c.Data["IsNothingToCompare"] = true
 		return true
 	}
 
-	diff, err := db.GetDiffRange(db.RepoPath(headUser.Name, headRepo.Name),
-		prInfo.MergeBase, headCommitID, conf.Git.MaxGitDiffLines,
-		conf.Git.MaxGitDiffLineCharacters, conf.Git.MaxGitDiffFiles)
+	diff, err := gitutil.RepoDiff(headGitRepo,
+		headCommitID, conf.Git.MaxDiffFiles, conf.Git.MaxDiffLines, conf.Git.MaxDiffLineChars,
+		git.DiffOptions{Base: meta.MergeBase},
+	)
 	if err != nil {
-		c.ServerError("GetDiffRange", err)
+		c.ServerError("get repository diff", err)
 		return false
 	}
 	c.Data["Diff"] = diff
 	c.Data["DiffNotAvailable"] = diff.NumFiles() == 0
 
-	headCommit, err := headGitRepo.GetCommit(headCommitID)
+	headCommit, err := headGitRepo.CatFileCommit(headCommitID)
 	if err != nil {
-		c.ServerError("GetCommit", err)
+		c.ServerError("get head commit", err)
 		return false
 	}
 
-	prInfo.Commits = db.ValidateCommitsWithEmails(prInfo.Commits)
-	c.Data["Commits"] = prInfo.Commits
-	c.Data["CommitCount"] = prInfo.Commits.Len()
+	c.Data["Commits"] = db.ValidateCommitsWithEmails(meta.Commits)
+	c.Data["CommitCount"] = len(meta.Commits)
 	c.Data["Username"] = headUser.Name
 	c.Data["Reponame"] = headRepo.Name
 	c.Data["IsImageFile"] = headCommit.IsImageFile
 
 	headTarget := path.Join(headUser.Name, repo.Name)
 	c.Data["SourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", headCommitID)
-	c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", prInfo.MergeBase)
+	c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", meta.MergeBase)
 	c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "raw", headCommitID)
 	return false
 }
@@ -625,7 +629,7 @@ func CompareAndPullRequest(c *context.Context) {
 		return
 	}
 
-	nothingToCompare := PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch)
+	nothingToCompare := PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, headBranch)
 	if c.Written() {
 		return
 	}
@@ -667,7 +671,7 @@ func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) {
 		attachments []string
 	)
 
-	headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch := ParseCompareInfo(c)
+	headUser, headRepo, headGitRepo, meta, baseBranch, headBranch := ParseCompareInfo(c)
 	if c.Written() {
 		return
 	}
@@ -686,7 +690,7 @@ func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) {
 
 		// This stage is already stop creating new pull request, so it does not matter if it has
 		// something to compare or not.
-		PrepareCompareDiff(c, headUser, headRepo, headGitRepo, prInfo, baseBranch, headBranch)
+		PrepareCompareDiff(c, headUser, headRepo, headGitRepo, meta, headBranch)
 		if c.Written() {
 			return
 		}
@@ -695,9 +699,9 @@ func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) {
 		return
 	}
 
-	patch, err := headGitRepo.GetPatch(prInfo.MergeBase, headBranch)
+	patch, err := headGitRepo.DiffBinary(meta.MergeBase, headBranch)
 	if err != nil {
-		c.ServerError("GetPatch", err)
+		c.ServerError("get patch", err)
 		return
 	}
 
@@ -720,7 +724,7 @@ func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) {
 		BaseBranch:   baseBranch,
 		HeadRepo:     headRepo,
 		BaseRepo:     repo,
-		MergeBase:    prInfo.MergeBase,
+		MergeBase:    meta.MergeBase,
 		Type:         db.PULL_REQUEST_GOGS,
 	}
 	// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt

+ 24 - 22
internal/route/repo/release.go

@@ -8,13 +8,15 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/gogs/git-module"
 	log "unknwon.dev/clog/v2"
 
+	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/form"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/markup"
-	"gogs.io/gogs/internal/conf"
 )
 
 const (
@@ -26,14 +28,14 @@ const (
 func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *db.Release, countCache map[string]int64) error {
 	// Get count if not exists
 	if _, ok := countCache[release.Target]; !ok {
-		if repoCtx.GitRepo.IsBranchExist(release.Target) {
-			commit, err := repoCtx.GitRepo.GetBranchCommit(release.Target)
+		if repoCtx.GitRepo.HasBranch(release.Target) {
+			commit, err := repoCtx.GitRepo.BranchCommit(release.Target)
 			if err != nil {
-				return fmt.Errorf("GetBranchCommit: %v", err)
+				return fmt.Errorf("get branch commit: %v", err)
 			}
 			countCache[release.Target], err = commit.CommitsCount()
 			if err != nil {
-				return fmt.Errorf("CommitsCount: %v", err)
+				return fmt.Errorf("count commits: %v", err)
 			}
 		} else {
 			// Use NumCommits of the newest release on that target
@@ -49,13 +51,13 @@ func Releases(c *context.Context) {
 	c.Data["PageIsViewFiles"] = true
 	c.Data["PageIsReleaseList"] = true
 
-	tagsResult, err := c.Repo.GitRepo.GetTagsAfter(c.Query("after"), 10)
+	tagsPage, err := gitutil.Module.ListTagsAfter(c.Repo.GitRepo.Path(), c.Query("after"), 10)
 	if err != nil {
-		c.Handle(500, fmt.Sprintf("GetTags '%s'", c.Repo.Repository.RepoPath()), err)
+		c.ServerError("get tags", err)
 		return
 	}
 
-	releases, err := db.GetPublishedReleasesByRepoID(c.Repo.Repository.ID, tagsResult.Tags...)
+	releases, err := db.GetPublishedReleasesByRepoID(c.Repo.Repository.ID, tagsPage.Tags...)
 	if err != nil {
 		c.Handle(500, "GetPublishedReleasesByRepoID", err)
 		return
@@ -64,8 +66,8 @@ func Releases(c *context.Context) {
 	// Temproray cache commits count of used branches to speed up.
 	countCache := make(map[string]int64)
 
-	results := make([]*db.Release, len(tagsResult.Tags))
-	for i, rawTag := range tagsResult.Tags {
+	results := make([]*db.Release, len(tagsPage.Tags))
+	for i, rawTag := range tagsPage.Tags {
 		for j, r := range releases {
 			if r == nil || r.TagName != rawTag {
 				continue
@@ -89,9 +91,9 @@ func Releases(c *context.Context) {
 
 		// No published release matches this tag
 		if results[i] == nil {
-			commit, err := c.Repo.GitRepo.GetTagCommit(rawTag)
+			commit, err := c.Repo.GitRepo.TagCommit(rawTag)
 			if err != nil {
-				c.Handle(500, "GetTagCommit", err)
+				c.Handle(500, "get tag commit", err)
 				return
 			}
 
@@ -103,7 +105,7 @@ func Releases(c *context.Context) {
 
 			results[i].NumCommits, err = commit.CommitsCount()
 			if err != nil {
-				c.Handle(500, "CommitsCount", err)
+				c.ServerError("count commits", err)
 				return
 			}
 			results[i].NumCommitsBehind = c.Repo.CommitsCount - results[i].NumCommits
@@ -113,7 +115,7 @@ func Releases(c *context.Context) {
 
 	// Only show drafts if user is viewing the latest page
 	var drafts []*db.Release
-	if tagsResult.HasLatest {
+	if tagsPage.HasLatest {
 		drafts, err = db.GetDraftReleasesByRepoID(c.Repo.Repository.ID)
 		if err != nil {
 			c.Handle(500, "GetDraftReleasesByRepoID", err)
@@ -140,9 +142,9 @@ func Releases(c *context.Context) {
 	}
 
 	c.Data["Releases"] = results
-	c.Data["HasPrevious"] = !tagsResult.HasLatest
-	c.Data["ReachEnd"] = tagsResult.ReachEnd
-	c.Data["PreviousAfter"] = tagsResult.PreviousAfter
+	c.Data["HasPrevious"] = !tagsPage.HasLatest
+	c.Data["ReachEnd"] = !tagsPage.HasNext
+	c.Data["PreviousAfter"] = tagsPage.PreviousAfter
 	if len(results) > 0 {
 		c.Data["NextAfter"] = results[len(results)-1].TagName
 	}
@@ -175,14 +177,14 @@ func NewReleasePost(c *context.Context, f form.NewRelease) {
 		return
 	}
 
-	if !c.Repo.GitRepo.IsBranchExist(f.Target) {
+	if !c.Repo.GitRepo.HasBranch(f.Target) {
 		c.RenderWithErr(c.Tr("form.target_branch_not_exist"), RELEASE_NEW, &f)
 		return
 	}
 
 	// Use current time if tag not yet exist, otherwise get time from Git
 	var tagCreatedUnix int64
-	tag, err := c.Repo.GitRepo.GetTag(f.TagName)
+	tag, err := c.Repo.GitRepo.Tag(git.RefsTags + f.TagName)
 	if err == nil {
 		commit, err := tag.Commit()
 		if err == nil {
@@ -190,15 +192,15 @@ func NewReleasePost(c *context.Context, f form.NewRelease) {
 		}
 	}
 
-	commit, err := c.Repo.GitRepo.GetBranchCommit(f.Target)
+	commit, err := c.Repo.GitRepo.BranchCommit(f.Target)
 	if err != nil {
-		c.Handle(500, "GetBranchCommit", err)
+		c.ServerError("get branch commit", err)
 		return
 	}
 
 	commitsCount, err := commit.CommitsCount()
 	if err != nil {
-		c.Handle(500, "CommitsCount", err)
+		c.ServerError("count commits", err)
 		return
 	}
 

+ 19 - 18
internal/route/repo/repo.go

@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"os"
 	"path"
+	"path/filepath"
 	"strings"
 
 	"github.com/unknwon/com"
@@ -271,22 +272,22 @@ func Action(c *context.Context) {
 
 func Download(c *context.Context) {
 	var (
-		uri         = c.Params("*")
-		refName     string
-		ext         string
-		archivePath string
-		archiveType git.ArchiveType
+		uri           = c.Params("*")
+		refName       string
+		ext           string
+		archivePath   string
+		archiveFormat git.ArchiveFormat
 	)
 
 	switch {
 	case strings.HasSuffix(uri, ".zip"):
 		ext = ".zip"
-		archivePath = path.Join(c.Repo.GitRepo.Path, "archives/zip")
-		archiveType = git.ZIP
+		archivePath = filepath.Join(c.Repo.GitRepo.Path(), "archives", "zip")
+		archiveFormat = git.ArchiveZip
 	case strings.HasSuffix(uri, ".tar.gz"):
 		ext = ".tar.gz"
-		archivePath = path.Join(c.Repo.GitRepo.Path, "archives/targz")
-		archiveType = git.TARGZ
+		archivePath = filepath.Join(c.Repo.GitRepo.Path(), "archives", "targz")
+		archiveFormat = git.ArchiveTarGz
 	default:
 		log.Trace("Unknown format: %s", uri)
 		c.Error(404)
@@ -307,20 +308,20 @@ func Download(c *context.Context) {
 		err    error
 	)
 	gitRepo := c.Repo.GitRepo
-	if gitRepo.IsBranchExist(refName) {
-		commit, err = gitRepo.GetBranchCommit(refName)
+	if gitRepo.HasBranch(refName) {
+		commit, err = gitRepo.BranchCommit(refName)
 		if err != nil {
-			c.Handle(500, "GetBranchCommit", err)
+			c.ServerError("get branch commit", err)
 			return
 		}
-	} else if gitRepo.IsTagExist(refName) {
-		commit, err = gitRepo.GetTagCommit(refName)
+	} else if gitRepo.HasTag(refName) {
+		commit, err = gitRepo.TagCommit(refName)
 		if err != nil {
-			c.Handle(500, "GetTagCommit", err)
+			c.ServerError("get tag commit", err)
 			return
 		}
 	} else if len(refName) >= 7 && len(refName) <= 40 {
-		commit, err = gitRepo.GetCommit(refName)
+		commit, err = gitRepo.CatFileCommit(refName)
 		if err != nil {
 			c.NotFound()
 			return
@@ -332,8 +333,8 @@ func Download(c *context.Context) {
 
 	archivePath = path.Join(archivePath, tool.ShortSHA1(commit.ID.String())+ext)
 	if !com.IsFile(archivePath) {
-		if err := commit.CreateArchive(archivePath, archiveType); err != nil {
-			c.Handle(500, "Download -> CreateArchive "+archivePath, err)
+		if err := commit.CreateArchive(archiveFormat, archivePath); err != nil {
+			c.ServerError("creates archive", err)
 			return
 		}
 	}

+ 14 - 17
internal/route/repo/setting.go

@@ -7,6 +7,7 @@ package repo
 import (
 	"fmt"
 	"io/ioutil"
+	"os"
 	"strings"
 	"time"
 
@@ -448,7 +449,7 @@ func SettingsBranches(c *context.Context) {
 	// Filter out deleted branches
 	branches := make([]string, 0, len(protectBranches))
 	for i := range protectBranches {
-		if c.Repo.GitRepo.IsBranchExist(protectBranches[i].Name) {
+		if c.Repo.GitRepo.HasBranch(protectBranches[i].Name) {
 			branches = append(branches, protectBranches[i].Name)
 		}
 	}
@@ -459,15 +460,12 @@ func SettingsBranches(c *context.Context) {
 
 func UpdateDefaultBranch(c *context.Context) {
 	branch := c.Query("branch")
-	if c.Repo.GitRepo.IsBranchExist(branch) &&
+	if c.Repo.GitRepo.HasBranch(branch) &&
 		c.Repo.Repository.DefaultBranch != branch {
 		c.Repo.Repository.DefaultBranch = branch
-		if err := c.Repo.GitRepo.SetDefaultBranch(branch); err != nil {
-			if !git.IsErrUnsupportedVersion(err) {
-				c.Handle(500, "SetDefaultBranch", err)
-				return
-			}
-
+		if _, err := c.Repo.GitRepo.SymbolicRef(git.SymbolicRefOptions{
+			Ref: git.RefsHeads + branch,
+		}); err != nil {
 			c.Flash.Warning(c.Tr("repo.settings.update_default_branch_unsupported"))
 			c.Redirect(c.Repo.RepoLink + "/settings/branches")
 			return
@@ -485,7 +483,7 @@ func UpdateDefaultBranch(c *context.Context) {
 
 func SettingsProtectedBranch(c *context.Context) {
 	branch := c.Params("*")
-	if !c.Repo.GitRepo.IsBranchExist(branch) {
+	if !c.Repo.GitRepo.HasBranch(branch) {
 		c.NotFound()
 		return
 	}
@@ -530,7 +528,7 @@ func SettingsProtectedBranch(c *context.Context) {
 
 func SettingsProtectedBranchPost(c *context.Context, f form.ProtectBranch) {
 	branch := c.Params("*")
-	if !c.Repo.GitRepo.IsBranchExist(branch) {
+	if !c.Repo.GitRepo.HasBranch(branch) {
 		c.NotFound()
 		return
 	}
@@ -570,7 +568,7 @@ func SettingsGitHooks(c *context.Context) {
 	c.Data["Title"] = c.Tr("repo.settings.githooks")
 	c.Data["PageIsSettingsGitHooks"] = true
 
-	hooks, err := c.Repo.GitRepo.Hooks()
+	hooks, err := c.Repo.GitRepo.Hooks("custom_hooks")
 	if err != nil {
 		c.Handle(500, "Hooks", err)
 		return
@@ -586,9 +584,9 @@ func SettingsGitHooksEdit(c *context.Context) {
 	c.Data["RequireSimpleMDE"] = true
 
 	name := c.Params(":name")
-	hook, err := c.Repo.GitRepo.GetHook(name)
+	hook, err := c.Repo.GitRepo.Hook("custom_hooks", git.HookName(name))
 	if err != nil {
-		if err == git.ErrNotValidHook {
+		if err == os.ErrNotExist {
 			c.Handle(404, "GetHook", err)
 		} else {
 			c.Handle(500, "GetHook", err)
@@ -601,17 +599,16 @@ func SettingsGitHooksEdit(c *context.Context) {
 
 func SettingsGitHooksEditPost(c *context.Context) {
 	name := c.Params(":name")
-	hook, err := c.Repo.GitRepo.GetHook(name)
+	hook, err := c.Repo.GitRepo.Hook("custom_hooks", git.HookName(name))
 	if err != nil {
-		if err == git.ErrNotValidHook {
+		if err == os.ErrNotExist {
 			c.Handle(404, "GetHook", err)
 		} else {
 			c.Handle(500, "GetHook", err)
 		}
 		return
 	}
-	hook.Content = c.Query("content")
-	if err = hook.Update(); err != nil {
+	if err = hook.Update(c.Query("content")); err != nil {
 		c.Handle(500, "hook.Update", err)
 		return
 	}

+ 40 - 49
internal/route/repo/view.go

@@ -8,19 +8,20 @@ import (
 	"bytes"
 	"fmt"
 	gotemplate "html/template"
-	"io/ioutil"
 	"path"
 	"strings"
+	"time"
 
+	"github.com/gogs/git-module"
+	"github.com/pkg/errors"
 	"github.com/unknwon/paginater"
 	log "unknwon.dev/clog/v2"
 
-	"github.com/gogs/git-module"
-
+	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/markup"
-	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/template"
 	"gogs.io/gogs/internal/template/highlight"
 	"gogs.io/gogs/internal/tool"
@@ -34,32 +35,36 @@ const (
 )
 
 func renderDirectory(c *context.Context, treeLink string) {
-	tree, err := c.Repo.Commit.SubTree(c.Repo.TreePath)
+	tree, err := c.Repo.Commit.Subtree(c.Repo.TreePath)
 	if err != nil {
-		c.NotFoundOrServerError("Repo.Commit.SubTree", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get subtree", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
-	entries, err := tree.ListEntries()
+	entries, err := tree.Entries()
 	if err != nil {
-		c.ServerError("ListEntries", err)
+		c.ServerError("list entries", err)
 		return
 	}
 	entries.Sort()
 
-	c.Data["Files"], err = entries.GetCommitsInfoWithCustomConcurrency(c.Repo.Commit, c.Repo.TreePath, conf.Repository.CommitsFetchConcurrency)
+	c.Data["Files"], err = entries.CommitsInfo(c.Repo.Commit, git.CommitsInfoOptions{
+		Path:           c.Repo.TreePath,
+		MaxConcurrency: conf.Repository.CommitsFetchConcurrency,
+		Timeout:        5 * time.Minute,
+	})
 	if err != nil {
-		c.ServerError("GetCommitsInfoWithCustomConcurrency", err)
+		c.ServerError("get commits info", err)
 		return
 	}
 
 	var readmeFile *git.Blob
 	for _, entry := range entries {
-		if entry.IsDir() || !markup.IsReadmeFile(entry.Name()) {
+		if entry.IsTree() || !markup.IsReadmeFile(entry.Name()) {
 			continue
 		}
 
-		// TODO: collect all possible README files and show with priority.
+		// TODO(unknwon): collect all possible README files and show with priority.
 		readmeFile = entry.Blob()
 		break
 	}
@@ -69,37 +74,30 @@ func renderDirectory(c *context.Context, treeLink string) {
 		c.Data["ReadmeInList"] = true
 		c.Data["ReadmeExist"] = true
 
-		dataRc, err := readmeFile.Data()
+		p, err := readmeFile.Bytes()
 		if err != nil {
 			c.ServerError("readmeFile.Data", err)
 			return
 		}
 
-		buf := make([]byte, 1024)
-		n, _ := dataRc.Read(buf)
-		buf = buf[:n]
-
-		isTextFile := tool.IsTextFile(buf)
+		isTextFile := tool.IsTextFile(p)
 		c.Data["IsTextFile"] = isTextFile
 		c.Data["FileName"] = readmeFile.Name()
 		if isTextFile {
-			d, _ := ioutil.ReadAll(dataRc)
-			buf = append(buf, d...)
-
 			switch markup.Detect(readmeFile.Name()) {
 			case markup.MARKDOWN:
 				c.Data["IsMarkdown"] = true
-				buf = markup.Markdown(buf, treeLink, c.Repo.Repository.ComposeMetas())
+				p = markup.Markdown(p, treeLink, c.Repo.Repository.ComposeMetas())
 			case markup.ORG_MODE:
 				c.Data["IsMarkdown"] = true
-				buf = markup.OrgMode(buf, treeLink, c.Repo.Repository.ComposeMetas())
+				p = markup.OrgMode(p, treeLink, c.Repo.Repository.ComposeMetas())
 			case markup.IPYTHON_NOTEBOOK:
 				c.Data["IsIPythonNotebook"] = true
 				c.Data["RawFileLink"] = c.Repo.RepoLink + "/raw/" + path.Join(c.Repo.BranchName, c.Repo.TreePath, readmeFile.Name())
 			default:
-				buf = bytes.Replace(buf, []byte("\n"), []byte(`<br>`), -1)
+				p = bytes.Replace(p, []byte("\n"), []byte(`<br>`), -1)
 			}
-			c.Data["FileContent"] = string(buf)
+			c.Data["FileContent"] = string(p)
 		}
 	}
 
@@ -107,9 +105,9 @@ func renderDirectory(c *context.Context, treeLink string) {
 	// or of directory if not in root directory.
 	latestCommit := c.Repo.Commit
 	if len(c.Repo.TreePath) > 0 {
-		latestCommit, err = c.Repo.Commit.GetCommitByPath(c.Repo.TreePath)
+		latestCommit, err = c.Repo.Commit.CommitByPath(git.CommitByRevisionOptions{Path: c.Repo.TreePath})
 		if err != nil {
-			c.ServerError("GetCommitByPath", err)
+			c.ServerError("get commit by path", err)
 			return
 		}
 	}
@@ -126,7 +124,7 @@ func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink stri
 	c.Data["IsViewFile"] = true
 
 	blob := entry.Blob()
-	dataRc, err := blob.Data()
+	p, err := blob.Bytes()
 	if err != nil {
 		c.Handle(500, "Data", err)
 		return
@@ -137,11 +135,7 @@ func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink stri
 	c.Data["HighlightClass"] = highlight.FileNameToHighlightClass(blob.Name())
 	c.Data["RawFileLink"] = rawLink + "/" + c.Repo.TreePath
 
-	buf := make([]byte, 1024)
-	n, _ := dataRc.Read(buf)
-	buf = buf[:n]
-
-	isTextFile := tool.IsTextFile(buf)
+	isTextFile := tool.IsTextFile(p)
 	c.Data["IsTextFile"] = isTextFile
 
 	// Assume file is not editable first.
@@ -159,26 +153,23 @@ func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink stri
 
 		c.Data["ReadmeExist"] = markup.IsReadmeFile(blob.Name())
 
-		d, _ := ioutil.ReadAll(dataRc)
-		buf = append(buf, d...)
-
 		switch markup.Detect(blob.Name()) {
 		case markup.MARKDOWN:
 			c.Data["IsMarkdown"] = true
-			c.Data["FileContent"] = string(markup.Markdown(buf, path.Dir(treeLink), c.Repo.Repository.ComposeMetas()))
+			c.Data["FileContent"] = string(markup.Markdown(p, path.Dir(treeLink), c.Repo.Repository.ComposeMetas()))
 		case markup.ORG_MODE:
 			c.Data["IsMarkdown"] = true
-			c.Data["FileContent"] = string(markup.OrgMode(buf, path.Dir(treeLink), c.Repo.Repository.ComposeMetas()))
+			c.Data["FileContent"] = string(markup.OrgMode(p, path.Dir(treeLink), c.Repo.Repository.ComposeMetas()))
 		case markup.IPYTHON_NOTEBOOK:
 			c.Data["IsIPythonNotebook"] = true
 		default:
 			// Building code view blocks with line number on server side.
 			var fileContent string
-			if err, content := template.ToUTF8WithErr(buf); err != nil {
+			if err, content := template.ToUTF8WithErr(p); err != nil {
 				if err != nil {
 					log.Error("ToUTF8WithErr: %s", err)
 				}
-				fileContent = string(buf)
+				fileContent = string(p)
 			} else {
 				fileContent = content
 			}
@@ -210,11 +201,11 @@ func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink stri
 			c.Data["EditFileTooltip"] = c.Tr("repo.editor.fork_before_edit")
 		}
 
-	case tool.IsPDFFile(buf):
+	case tool.IsPDFFile(p):
 		c.Data["IsPDFFile"] = true
-	case tool.IsVideoFile(buf):
+	case tool.IsVideoFile(p):
 		c.Data["IsVideoFile"] = true
-	case tool.IsImageFile(buf):
+	case tool.IsImageFile(p):
 		c.Data["IsImageFile"] = true
 	}
 
@@ -229,9 +220,9 @@ func renderFile(c *context.Context, entry *git.TreeEntry, treeLink, rawLink stri
 }
 
 func setEditorconfigIfExists(c *context.Context) {
-	ec, err := c.Repo.GetEditorconfig()
-	if err != nil && !git.IsErrNotExist(err) {
-		log.Trace("setEditorconfigIfExists.GetEditorconfig [%d]: %v", c.Repo.Repository.ID, err)
+	ec, err := c.Repo.Editorconfig()
+	if err != nil && !gitutil.IsErrRevisionNotExist(errors.Cause(err)) {
+		log.Warn("setEditorconfigIfExists.Editorconfig [repo_id: %d]: %v", c.Repo.Repository.ID, err)
 		return
 	}
 	c.Data["Editorconfig"] = ec
@@ -277,13 +268,13 @@ func Home(c *context.Context) {
 	c.Data["PageIsRepoHome"] = isRootDir
 
 	// Get current entry user currently looking at.
-	entry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath)
+	entry, err := c.Repo.Commit.TreeEntry(c.Repo.TreePath)
 	if err != nil {
-		c.NotFoundOrServerError("Repo.Commit.GetTreeEntryByPath", git.IsErrNotExist, err)
+		c.NotFoundOrServerError("get tree entry", gitutil.IsErrRevisionNotExist, err)
 		return
 	}
 
-	if entry.IsDir() {
+	if entry.IsTree() {
 		renderDirectory(c, treeLink)
 	} else {
 		renderFile(c, entry, treeLink, rawLink)

+ 45 - 31
internal/route/repo/webhook.go

@@ -14,11 +14,11 @@ import (
 	git "github.com/gogs/git-module"
 	api "github.com/gogs/go-gogs-client"
 
+	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/db/errors"
 	"gogs.io/gogs/internal/form"
-	"gogs.io/gogs/internal/conf"
 )
 
 const (
@@ -517,23 +517,37 @@ func DingtalkHooksEditPost(c *context.Context, f form.NewDingtalkHook) {
 }
 
 func TestWebhook(c *context.Context) {
-	var authorUsername, committerUsername string
+
+	var (
+		commitID          string
+		commitMessage     string
+		author            *git.Signature
+		committer         *git.Signature
+		authorUsername    string
+		committerUsername string
+		nameStatus        *git.NameStatus
+	)
 
 	// Grab latest commit or fake one if it's empty repository.
-	commit := c.Repo.Commit
-	if commit == nil {
+
+	if c.Repo.Commit == nil {
+		commitID = git.EmptyID
+		commitMessage = "This is a fake commit"
 		ghost := db.NewGhostUser()
-		commit = &git.Commit{
-			ID:            git.MustIDFromString(git.EMPTY_SHA),
-			Author:        ghost.NewGitSig(),
-			Committer:     ghost.NewGitSig(),
-			CommitMessage: "This is a fake commit",
-		}
+		author = ghost.NewGitSig()
+		committer = ghost.NewGitSig()
 		authorUsername = ghost.Name
 		committerUsername = ghost.Name
+		nameStatus = &git.NameStatus{}
+
 	} else {
+		commitID = c.Repo.Commit.ID.String()
+		commitMessage = c.Repo.Commit.Message
+		author = c.Repo.Commit.Author
+		committer = c.Repo.Commit.Committer
+
 		// Try to match email with a real user.
-		author, err := db.GetUserByEmail(commit.Author.Email)
+		author, err := db.GetUserByEmail(c.Repo.Commit.Author.Email)
 		if err == nil {
 			authorUsername = author.Name
 		} else if !errors.IsUserNotExist(err) {
@@ -541,44 +555,44 @@ func TestWebhook(c *context.Context) {
 			return
 		}
 
-		committer, err := db.GetUserByEmail(commit.Committer.Email)
+		user, err := db.GetUserByEmail(c.Repo.Commit.Committer.Email)
 		if err == nil {
-			committerUsername = committer.Name
+			committerUsername = user.Name
 		} else if !errors.IsUserNotExist(err) {
 			c.Handle(500, "GetUserByEmail.(committer)", err)
 			return
 		}
-	}
 
-	fileStatus, err := commit.FileStatus()
-	if err != nil {
-		c.Handle(500, "FileStatus", err)
-		return
+		nameStatus, err = c.Repo.Commit.ShowNameStatus()
+		if err != nil {
+			c.Handle(500, "FileStatus", err)
+			return
+		}
 	}
 
 	apiUser := c.User.APIFormat()
 	p := &api.PushPayload{
-		Ref:    git.BRANCH_PREFIX + c.Repo.Repository.DefaultBranch,
-		Before: commit.ID.String(),
-		After:  commit.ID.String(),
+		Ref:    git.RefsHeads + c.Repo.Repository.DefaultBranch,
+		Before: commitID,
+		After:  commitID,
 		Commits: []*api.PayloadCommit{
 			{
-				ID:      commit.ID.String(),
-				Message: commit.Message(),
-				URL:     c.Repo.Repository.HTMLURL() + "/commit/" + commit.ID.String(),
+				ID:      commitID,
+				Message: commitMessage,
+				URL:     c.Repo.Repository.HTMLURL() + "/commit/" + commitID,
 				Author: &api.PayloadUser{
-					Name:     commit.Author.Name,
-					Email:    commit.Author.Email,
+					Name:     author.Name,
+					Email:    author.Email,
 					UserName: authorUsername,
 				},
 				Committer: &api.PayloadUser{
-					Name:     commit.Committer.Name,
-					Email:    commit.Committer.Email,
+					Name:     committer.Name,
+					Email:    committer.Email,
 					UserName: committerUsername,
 				},
-				Added:    fileStatus.Added,
-				Removed:  fileStatus.Removed,
-				Modified: fileStatus.Modified,
+				Added:    nameStatus.Added,
+				Removed:  nameStatus.Removed,
+				Modified: nameStatus.Modified,
 			},
 		},
 		Repo:   c.Repo.Repository.APIFormat(nil),

+ 31 - 36
internal/route/repo/wiki.go

@@ -5,7 +5,6 @@
 package repo
 
 import (
-	"io/ioutil"
 	"strings"
 	"time"
 
@@ -14,6 +13,7 @@ import (
 	"gogs.io/gogs/internal/context"
 	"gogs.io/gogs/internal/db"
 	"gogs.io/gogs/internal/form"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/markup"
 )
 
@@ -43,27 +43,27 @@ type PageMeta struct {
 }
 
 func renderWikiPage(c *context.Context, isViewPage bool) (*git.Repository, string) {
-	wikiRepo, err := git.OpenRepository(c.Repo.Repository.WikiPath())
+	wikiRepo, err := git.Open(c.Repo.Repository.WikiPath())
 	if err != nil {
-		c.Handle(500, "OpenRepository", err)
+		c.ServerError("open repository", err)
 		return nil, ""
 	}
-	commit, err := wikiRepo.GetBranchCommit("master")
+	commit, err := wikiRepo.BranchCommit("master")
 	if err != nil {
-		c.Handle(500, "GetBranchCommit", err)
+		c.ServerError("get branch commit", err)
 		return nil, ""
 	}
 
 	// Get page list.
 	if isViewPage {
-		entries, err := commit.ListEntries()
+		entries, err := commit.Entries()
 		if err != nil {
-			c.Handle(500, "ListEntries", err)
+			c.ServerError("list entries", err)
 			return nil, ""
 		}
 		pages := make([]PageMeta, 0, len(entries))
 		for i := range entries {
-			if entries[i].Type == git.OBJECT_BLOB && strings.HasSuffix(entries[i].Name(), ".md") {
+			if entries[i].Type() == git.ObjectBlob && strings.HasSuffix(entries[i].Name(), ".md") {
 				name := strings.TrimSuffix(entries[i].Name(), ".md")
 				pages = append(pages, PageMeta{
 					Name: name,
@@ -86,29 +86,24 @@ func renderWikiPage(c *context.Context, isViewPage bool) (*git.Repository, strin
 	c.Data["title"] = pageName
 	c.Data["RequireHighlightJS"] = true
 
-	blob, err := commit.GetBlobByPath(pageName + ".md")
+	blob, err := commit.Blob(pageName + ".md")
 	if err != nil {
-		if git.IsErrNotExist(err) {
+		if gitutil.IsErrRevisionNotExist(err) {
 			c.Redirect(c.Repo.RepoLink + "/wiki/_pages")
 		} else {
-			c.Handle(500, "GetBlobByPath", err)
+			c.ServerError("GetBlobByPath", err)
 		}
 		return nil, ""
 	}
-	r, err := blob.Data()
+	p, err := blob.Bytes()
 	if err != nil {
-		c.Handle(500, "Data", err)
-		return nil, ""
-	}
-	data, err := ioutil.ReadAll(r)
-	if err != nil {
-		c.Handle(500, "ReadAll", err)
+		c.ServerError("Data", err)
 		return nil, ""
 	}
 	if isViewPage {
-		c.Data["content"] = string(markup.Markdown(data, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas()))
+		c.Data["content"] = string(markup.Markdown(p, c.Repo.RepoLink, c.Repo.Repository.ComposeMetas()))
 	} else {
-		c.Data["content"] = string(data)
+		c.Data["content"] = string(p)
 	}
 
 	return wikiRepo, pageName
@@ -129,12 +124,12 @@ func Wiki(c *context.Context) {
 	}
 
 	// Get last change information.
-	lastCommit, err := wikiRepo.GetCommitByPath(pageName + ".md")
+	commits, err := wikiRepo.Log(git.RefsHeads+"master", git.LogOptions{Path: pageName + ".md"})
 	if err != nil {
-		c.Handle(500, "GetCommitByPath", err)
+		c.ServerError("get commits by path", err)
 		return
 	}
-	c.Data["Author"] = lastCommit.Author
+	c.Data["Author"] = commits[0].Author
 
 	c.HTML(200, WIKI_VIEW)
 }
@@ -148,35 +143,35 @@ func WikiPages(c *context.Context) {
 		return
 	}
 
-	wikiRepo, err := git.OpenRepository(c.Repo.Repository.WikiPath())
+	wikiRepo, err := git.Open(c.Repo.Repository.WikiPath())
 	if err != nil {
-		c.Handle(500, "OpenRepository", err)
+		c.ServerError("open repository", err)
 		return
 	}
-	commit, err := wikiRepo.GetBranchCommit("master")
+	commit, err := wikiRepo.BranchCommit("master")
 	if err != nil {
-		c.Handle(500, "GetBranchCommit", err)
+		c.ServerError("get branch commit", err)
 		return
 	}
 
-	entries, err := commit.ListEntries()
+	entries, err := commit.Entries()
 	if err != nil {
-		c.Handle(500, "ListEntries", err)
+		c.ServerError("list entries", err)
 		return
 	}
 	pages := make([]PageMeta, 0, len(entries))
 	for i := range entries {
-		if entries[i].Type == git.OBJECT_BLOB && strings.HasSuffix(entries[i].Name(), ".md") {
-			commit, err := wikiRepo.GetCommitByPath(entries[i].Name())
+		if entries[i].Type() == git.ObjectBlob && strings.HasSuffix(entries[i].Name(), ".md") {
+			commits, err := wikiRepo.Log(git.RefsHeads+"master", git.LogOptions{Path: entries[i].Name()})
 			if err != nil {
-				c.ServerError("GetCommitByPath", err)
+				c.ServerError("get commits by path", err)
 				return
 			}
 			name := strings.TrimSuffix(entries[i].Name(), ".md")
 			pages = append(pages, PageMeta{
 				Name:    name,
 				URL:     db.ToWikiPageURL(name),
-				Updated: commit.Author.When,
+				Updated: commits[0].Author.When,
 			})
 		}
 	}
@@ -212,7 +207,7 @@ func NewWikiPost(c *context.Context, f form.NewWiki) {
 			c.Data["Err_Title"] = true
 			c.RenderWithErr(c.Tr("repo.wiki.page_already_exists"), WIKI_NEW, &f)
 		} else {
-			c.Handle(500, "AddWikiPage", err)
+			c.ServerError("AddWikiPage", err)
 		}
 		return
 	}
@@ -249,7 +244,7 @@ func EditWikiPost(c *context.Context, f form.NewWiki) {
 	}
 
 	if err := c.Repo.Repository.EditWikiPage(c.User, f.OldTitle, f.Title, f.Content, f.Message); err != nil {
-		c.Handle(500, "EditWikiPage", err)
+		c.ServerError("EditWikiPage", err)
 		return
 	}
 
@@ -264,7 +259,7 @@ func DeleteWikiPagePost(c *context.Context) {
 
 	pageName := db.ToWikiPageName(pageURL)
 	if err := c.Repo.Repository.DeleteWikiPage(c.User, pageName); err != nil {
-		c.Handle(500, "DeleteWikiPage", err)
+		c.ServerError("DeleteWikiPage", err)
 		return
 	}
 

+ 20 - 27
internal/template/template.go

@@ -5,7 +5,6 @@
 package template
 
 import (
-	"container/list"
 	"fmt"
 	"html/template"
 	"mime"
@@ -21,8 +20,11 @@ import (
 	"golang.org/x/text/transform"
 	log "unknwon.dev/clog/v2"
 
+	"github.com/gogs/git-module"
+
 	"gogs.io/gogs/internal/conf"
 	"gogs.io/gogs/internal/db"
+	"gogs.io/gogs/internal/gitutil"
 	"gogs.io/gogs/internal/markup"
 	"gogs.io/gogs/internal/tool"
 )
@@ -36,6 +38,9 @@ var (
 func FuncMap() []template.FuncMap {
 	funcMapOnce.Do(func() {
 		funcMap = []template.FuncMap{map[string]interface{}{
+			"BuildCommit": func() string {
+				return conf.BuildCommit
+			},
 			"Year": func() int {
 				return time.Now().Year()
 			},
@@ -86,7 +91,6 @@ func FuncMap() []template.FuncMap {
 			"DateFmtShort": func(t time.Time) string {
 				return t.Format("Jan 02, 2006")
 			},
-			"List": List,
 			"SubStr": func(str string, start, length int) string {
 				if len(str) == 0 {
 					return ""
@@ -102,11 +106,10 @@ func FuncMap() []template.FuncMap {
 			},
 			"Join":                  strings.Join,
 			"EllipsisString":        tool.EllipsisString,
-			"DiffTypeToStr":         DiffTypeToStr,
+			"DiffFileTypeToStr":     DiffFileTypeToStr,
 			"DiffLineTypeToStr":     DiffLineTypeToStr,
 			"Sha1":                  Sha1,
 			"ShortSHA1":             tool.ShortSHA1,
-			"MD5":                   tool.MD5,
 			"ActionContent2Commits": ActionContent2Commits,
 			"EscapePound":           EscapePound,
 			"RenderCommitMessage":   RenderCommitMessage,
@@ -126,6 +129,7 @@ func FuncMap() []template.FuncMap {
 				}
 				return "tab-size-8"
 			},
+			"InferSubmoduleURL": gitutil.InferSubmoduleURL,
 		}}
 	})
 	return funcMap
@@ -144,19 +148,6 @@ func NewLine2br(raw string) string {
 	return strings.Replace(raw, "\n", "<br>", -1)
 }
 
-func List(l *list.List) chan interface{} {
-	e := l.Front()
-	c := make(chan interface{})
-	go func() {
-		for e != nil {
-			c <- e.Value
-			e = e.Next()
-		}
-		close(c)
-	}()
-	return c
-}
-
 func Sha1(str string) string {
 	return tool.SHA1(str)
 }
@@ -269,20 +260,22 @@ func EscapePound(str string) string {
 	return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str)
 }
 
-func DiffTypeToStr(diffType int) string {
-	diffTypes := map[int]string{
-		1: "add", 2: "modify", 3: "del", 4: "rename",
-	}
-	return diffTypes[diffType]
+func DiffFileTypeToStr(typ git.DiffFileType) string {
+	return map[git.DiffFileType]string{
+		git.DiffFileAdd:    "add",
+		git.DiffFileChange: "modify",
+		git.DiffFileDelete: "del",
+		git.DiffFileRename: "rename",
+	}[typ]
 }
 
-func DiffLineTypeToStr(diffType int) string {
-	switch diffType {
-	case 2:
+func DiffLineTypeToStr(typ git.DiffLineType) string {
+	switch typ {
+	case git.DiffLineAdd:
 		return "add"
-	case 3:
+	case git.DiffLineDelete:
 		return "del"
-	case 4:
+	case git.DiffLineSection:
 		return "tag"
 	}
 	return "same"

+ 56 - 62
public/css/gogs.css

@@ -5,7 +5,7 @@
   background-size: contain;
 }
 body:not(.full-width) {
-  font-family: "PingFang SC", "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
+  font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
   background-color: #fff;
   overflow-y: scroll;
   overflow-x: auto;
@@ -23,14 +23,14 @@ h5,
 .ui.menu,
 .ui.input input,
 .ui.button:not(.label) {
-  font-family: "PingFang SC", 'Hiragino Sans GB', "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
+  font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
 }
 img {
   border-radius: 3px;
 }
 pre,
 code {
-  font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
 }
 pre.raw,
 code.raw {
@@ -75,7 +75,7 @@ code.wrap {
 }
 .following.bar.light {
   background-color: white;
-  border-bottom: 1px solid #DDDDDD;
+  border-bottom: 1px solid #dddddd;
   box-shadow: 0 2px 3px rgba(0, 0, 0, 0.04);
 }
 .following.bar .column .menu {
@@ -156,7 +156,7 @@ code.wrap {
   color: #d95c5c !important;
 }
 .ui .text.red a:hover {
-  color: #E67777 !important;
+  color: #e67777 !important;
 }
 .ui .text.blue {
   color: #428bca !important;
@@ -192,7 +192,7 @@ code.wrap {
   color: #6e5494 !important;
 }
 .ui .text.yellow {
-  color: #FBBD08 !important;
+  color: #fbbd08 !important;
 }
 .ui .text.gold {
   color: #a1882b !important;
@@ -235,11 +235,11 @@ code.wrap {
   vertical-align: middle;
 }
 .ui .warning.header {
-  background-color: #F9EDBE !important;
-  border-color: #F0C36D;
+  background-color: #f9edbe !important;
+  border-color: #f0c36d;
 }
 .ui .warning.segment {
-  border-color: #F0C36D;
+  border-color: #f0c36d;
 }
 .ui .info.segment {
   border: 1px solid #c5d5dd;
@@ -270,7 +270,7 @@ code.wrap {
   margin-left: 25px;
 }
 .ui .sha.label {
-  font-family: Consolas, Menlo, Monaco, "Lucida Console", monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
   font-size: 13px;
   padding: 6px 10px 4px 10px;
   font-weight: normal;
@@ -1185,7 +1185,7 @@ footer .ui.language .menu {
   overflow-x: auto;
 }
 .repository .metas .ui.list .hide {
-  display: none!important;
+  display: none !important;
 }
 .repository .metas .ui.list .item {
   padding: 0px;
@@ -1204,7 +1204,7 @@ footer .ui.language .menu {
   color: #000;
 }
 .repository .header-wrapper {
-  background-color: #FAFAFA;
+  background-color: #fafafa;
   margin-top: -15px;
   padding-top: 15px;
 }
@@ -1228,8 +1228,8 @@ footer .ui.language .menu {
 .repository .filter.menu .menu {
   max-height: 300px;
   overflow-x: auto;
-  right: 0!important;
-  left: auto!important;
+  right: 0 !important;
+  left: auto !important;
 }
 .repository .filter.menu .dropdown.item {
   margin: 1px;
@@ -1268,8 +1268,8 @@ footer .ui.language .menu {
   padding: 0 10px;
 }
 .repository #clone-panel .dropdown .menu {
-  right: 0!important;
-  left: auto!important;
+  right: 0 !important;
+  left: auto !important;
 }
 .repository.branches:not(.settings) .ui.list {
   padding: 0;
@@ -1279,7 +1279,7 @@ footer .ui.language .menu {
   line-height: 31px;
 }
 .repository.branches:not(.settings) .ui.list > .item:not(:last-child) {
-  border-bottom: 1px solid #DDD;
+  border-bottom: 1px solid #ddd;
 }
 .repository.branches:not(.settings) .ui.list > .item .column {
   padding: 5px 15px;
@@ -1355,7 +1355,7 @@ footer .ui.language .menu {
   padding-bottom: 8px;
 }
 .repository.file.list #repo-files-table tr:hover {
-  background-color: #ffffEE;
+  background-color: #ffffee;
 }
 .repository.file.list #file-content .header .octicon {
   padding-right: 5px;
@@ -1414,7 +1414,7 @@ footer .ui.language .menu {
   padding: 0.1em 0.5em;
 }
 .repository.file.list #file-content #ipython-notebook .nb-stderr {
-  background-color: #FAA;
+  background-color: #faa;
 }
 .repository.file.list #file-content #ipython-notebook .nb-cell + .nb-cell {
   margin-top: 0.5em;
@@ -1431,7 +1431,7 @@ footer .ui.language .menu {
 .repository.file.list #file-content #ipython-notebook .nb-raw-cell {
   white-space: pre-wrap;
   background-color: #f5f2f0;
-  font-family: Consolas, Monaco, 'Andale Mono', monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
   padding: 1em;
   margin: 0.5em 0;
 }
@@ -1497,7 +1497,7 @@ footer .ui.language .menu {
 }
 .repository.file.list #file-content .code-view * {
   font-size: 12px;
-  font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
   line-height: 20px;
 }
 .repository.file.list #file-content .code-view table {
@@ -1542,6 +1542,7 @@ footer .ui.language .menu {
 .repository.file.list #file-content .code-view .lines-code .hljs li {
   display: inline-block;
   width: 100%;
+  padding-left: 5px;
 }
 .repository.file.list #file-content .code-view .lines-num pre li.active,
 .repository.file.list #file-content .code-view .lines-code pre li.active,
@@ -1551,14 +1552,6 @@ footer .ui.language .menu {
 .repository.file.list #file-content .code-view .lines-code .hljs li.active {
   background: #ffffdd;
 }
-.repository.file.list #file-content .code-view .lines-num pre li:before,
-.repository.file.list #file-content .code-view .lines-code pre li:before,
-.repository.file.list #file-content .code-view .lines-num ol li:before,
-.repository.file.list #file-content .code-view .lines-code ol li:before,
-.repository.file.list #file-content .code-view .lines-num .hljs li:before,
-.repository.file.list #file-content .code-view .lines-code .hljs li:before {
-  content: ' ';
-}
 .repository.file.list .sidebar {
   padding-left: 0;
 }
@@ -1606,7 +1599,7 @@ footer .ui.language .menu {
   pointer-events: none;
 }
 .repository.file.editor .commit-form-wrapper .commit-form:before {
-  border-right-color: #D4D4D5;
+  border-right-color: #d4d4d5;
   border-width: 9px;
   margin-top: -9px;
 }
@@ -1621,7 +1614,7 @@ footer .ui.language .menu {
 .repository.file.editor .commit-form-wrapper .commit-form .quick-pull-choice .branch-name {
   display: inline-block;
   padding: 3px 6px;
-  font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+  font: 12px Consolas, Liberation Mono, Menlo, monospace;
   color: rgba(0, 0, 0, 0.65);
   background-color: rgba(209, 227, 237, 0.45);
   border-radius: 3px;
@@ -1641,7 +1634,7 @@ footer .ui.language .menu {
   color: #b0c4ce;
 }
 .repository.options #interval {
-  width: 100px!important;
+  width: 100px !important;
   min-width: 100px;
 }
 .repository.options .danger .item {
@@ -1668,7 +1661,7 @@ footer .ui.language .menu {
   pointer-events: none;
 }
 .repository.new.issue .comment.form .content:before {
-  border-right-color: #D4D4D5;
+  border-right-color: #d4d4d5;
   border-width: 9px;
   margin-top: -9px;
 }
@@ -1691,7 +1684,7 @@ footer .ui.language .menu {
   overflow-x: auto;
 }
 .repository.view.issue .title {
-  padding-bottom: 0!important;
+  padding-bottom: 0 !important;
 }
 .repository.view.issue .title h1 {
   font-weight: 300;
@@ -1720,7 +1713,7 @@ footer .ui.language .menu {
   margin-top: 10px;
 }
 .repository.view.issue .pull-desc code {
-  color: #0166E6;
+  color: #0166e6;
 }
 .repository.view.issue .pull.tabular.menu {
   margin-bottom: 10px;
@@ -1801,7 +1794,7 @@ footer .ui.language .menu {
   pointer-events: none;
 }
 .repository.view.issue .comment-list .comment .content .header:before {
-  border-right-color: #D4D4D5;
+  border-right-color: #d4d4d5;
   border-width: 9px;
   margin-top: -9px;
 }
@@ -1827,7 +1820,7 @@ footer .ui.language .menu {
 }
 .repository.view.issue .comment-list .comment .content > .bottom.segment .ui.images::after {
   clear: both;
-  content: ' ';
+  content: " ";
   display: block;
 }
 .repository.view.issue .comment-list .comment .content > .bottom.segment a {
@@ -1842,7 +1835,7 @@ footer .ui.language .menu {
   background-color: #fff;
 }
 .repository.view.issue .comment-list .comment .content > .bottom.segment a:before {
-  content: ' ';
+  content: " ";
   display: inline-block;
   height: 100%;
   vertical-align: middle;
@@ -1870,7 +1863,7 @@ footer .ui.language .menu {
 }
 .repository.view.issue .comment-list .comment .ui.form textarea {
   height: 200px;
-  font-family: "Consolas", monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
 }
 .repository.view.issue .comment-list .comment .edit.buttons {
   margin-top: 10px;
@@ -1937,7 +1930,7 @@ footer .ui.language .menu {
   pointer-events: none;
 }
 .repository .comment.form .content .form:before {
-  border-right-color: #D4D4D5;
+  border-right-color: #d4d4d5;
   border-width: 9px;
   margin-top: -9px;
 }
@@ -1956,7 +1949,7 @@ footer .ui.language .menu {
 }
 .repository .comment.form .content textarea {
   height: 200px;
-  font-family: "Consolas", monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
 }
 .repository .label.list {
   list-style: none;
@@ -1965,7 +1958,7 @@ footer .ui.language .menu {
 .repository .label.list > .item {
   padding-top: 10px;
   padding-bottom: 10px;
-  border-bottom: 1px dashed #AAA;
+  border-bottom: 1px dashed #aaa;
 }
 .repository .label.list > .item a {
   font-size: 15px;
@@ -1989,7 +1982,7 @@ footer .ui.language .menu {
 .repository .milestone.list > .item {
   padding-top: 10px;
   padding-bottom: 10px;
-  border-bottom: 1px dashed #AAA;
+  border-bottom: 1px dashed #aaa;
 }
 .repository .milestone.list > .item > a {
   padding-top: 5px;
@@ -2054,7 +2047,7 @@ footer .ui.language .menu {
   pointer-events: none;
 }
 .repository.compare.pull .comment.form .content:before {
-  border-right-color: #D4D4D5;
+  border-right-color: #d4d4d5;
   border-width: 9px;
   margin-top: -9px;
 }
@@ -2067,7 +2060,7 @@ footer .ui.language .menu {
   border-right-color: #fff;
 }
 .repository .filter.dropdown .menu {
-  margin-top: 1px!important;
+  margin-top: 1px !important;
 }
 .repository.diff .commit-message pre {
   white-space: pre-wrap;
@@ -2100,7 +2093,7 @@ footer .ui.language .menu {
   list-style: none;
   padding-bottom: 4px;
   margin-bottom: 4px;
-  border-bottom: 1px dashed #DDD;
+  border-bottom: 1px dashed #ddd;
   padding-left: 6px;
 }
 .repository .diff-detail-box span.status {
@@ -2146,7 +2139,7 @@ footer .ui.language .menu {
 }
 .repository .diff-file-box .file-body.file-code .lines-num {
   text-align: right;
-  color: #A7A7A7;
+  color: #a7a7a7;
   background: #fafafa;
   width: 1%;
 }
@@ -2155,7 +2148,7 @@ footer .ui.language .menu {
   text-align: center;
 }
 .repository .diff-file-box .file-body.file-code .lines-num-old {
-  border-right: 1px solid #DDD;
+  border-right: 1px solid #ddd;
 }
 .repository .diff-file-box .code-diff {
   font-size: 12px;
@@ -2175,6 +2168,7 @@ footer .ui.language .menu {
 }
 .repository .diff-file-box .code-diff .lines-num::before {
   content: attr(data-line-number);
+  font: Consolas, Liberation Mono, Menlo, monospace;
 }
 .repository .diff-file-box .code-diff .lines-num.lines-num-old,
 .repository .diff-file-box .code-diff .lines-num.lines-num-new {
@@ -2185,8 +2179,8 @@ footer .ui.language .menu {
   color: #383636;
 }
 .repository .diff-file-box .code-diff tbody tr.tag-code td {
-  background-color: #F0F0F0 !important;
-  border-color: #D2CECE !important;
+  background-color: #f0f0f0 !important;
+  border-color: #d2cece !important;
   padding-top: 4px;
   padding-bottom: 4px;
 }
@@ -2261,7 +2255,7 @@ footer .ui.language .menu {
   font-size: 1.2em;
 }
 .repository.release #release-list {
-  border-top: 1px solid #DDD;
+  border-top: 1px solid #ddd;
   margin-top: 20px;
   padding-top: 15px;
 }
@@ -2286,7 +2280,7 @@ footer .ui.language .menu {
   margin-top: 6px;
 }
 .repository.release #release-list > li .detail {
-  border-left: 1px solid #DDD;
+  border-left: 1px solid #ddd;
 }
 .repository.release #release-list > li .detail .author img {
   margin-bottom: -3px;
@@ -2319,7 +2313,7 @@ footer .ui.language .menu {
   left: -5px;
   top: 40px;
   border-radius: 6px;
-  border: 1px solid #FFF;
+  border: 1px solid #fff;
 }
 .repository.new.release .target {
   min-width: 500px;
@@ -2348,7 +2342,7 @@ footer .ui.language .menu {
 .repository.forks .list .item {
   padding-top: 10px;
   padding-bottom: 10px;
-  border-bottom: 1px solid #DDD;
+  border-bottom: 1px solid #ddd;
 }
 .repository.forks .list .item .ui.avatar {
   float: left;
@@ -2365,7 +2359,7 @@ footer .ui.language .menu {
   font-size: 48px;
 }
 .repository.wiki.new .CodeMirror .CodeMirror-code {
-  font-family: "Consolas", monospace;
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
 }
 .repository.wiki.new .CodeMirror .CodeMirror-code .cm-comment {
   background: inherit;
@@ -2399,7 +2393,7 @@ footer .ui.language .menu {
   line-height: 2em;
 }
 .repository.settings.collaboration .collaborator.list > .item:not(:last-child) {
-  border-bottom: 1px solid #DDD;
+  border-bottom: 1px solid #ddd;
 }
 .repository.settings.collaboration #repo-collab-form #search-user-box .results {
   left: 7px;
@@ -2491,7 +2485,7 @@ footer .ui.language .menu {
 #search-repo-box .results .item,
 #search-user-box .results .item {
   padding: 10px 15px;
-  border-bottom: 1px solid #DDD;
+  border-bottom: 1px solid #ddd;
   cursor: pointer;
 }
 #search-repo-box .results .item:hover,
@@ -2510,7 +2504,7 @@ footer .ui.language .menu {
 .issue.list > .item {
   padding-top: 15px;
   padding-bottom: 10px;
-  border-bottom: 1px dashed #AAA;
+  border-bottom: 1px dashed #aaa;
 }
 .issue.list > .item .title {
   color: #444;
@@ -2546,8 +2540,8 @@ footer .ui.language .menu {
 .ui.form .dropzone {
   width: 100%;
   margin-bottom: 10px;
-  border: 2px dashed #0087F7;
-  box-shadow: none!important;
+  border: 2px dashed #0087f7;
+  box-shadow: none !important;
 }
 .ui.form .dropzone .dz-error-message {
   top: 140px;
@@ -2688,7 +2682,7 @@ footer .ui.language .menu {
   pointer-events: none;
 }
 #avatar-arrow:before {
-  border-right-color: #D4D4D5;
+  border-right-color: #d4d4d5;
   border-width: 9px;
   margin-top: -9px;
 }
@@ -2699,7 +2693,7 @@ footer .ui.language .menu {
 }
 #transfer-repo-modal .ui.message,
 #delete-repo-modal .ui.message {
-  width: 100%!important;
+  width: 100% !important;
 }
 .tab-size-1 {
   tab-size: 1 !important;

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
public/css/gogs.css.map


+ 377 - 371
public/less/_base.less

@@ -1,41 +1,46 @@
 @footer-margin: 40px;
 
 body:not(.full-width) {
-	font-family: "PingFang SC", "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
-	background-color: #fff;
-	overflow-y: scroll;
-	overflow-x: auto;
-	min-width: 1020px;
+  font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
+  background-color: #fff;
+  overflow-y: scroll;
+  overflow-x: auto;
+  min-width: 1020px;
 }
 .ui.container:not(.fluid) {
-	width: 980px !important;
+  width: 980px !important;
 }
-h1, h2, h3, h4, h5,
+h1,
+h2,
+h3,
+h4,
+h5,
 .ui.header,
 .ui.menu,
 .ui.input input,
 .ui.button:not(.label) {
-	font-family: "PingFang SC", 'Hiragino Sans GB', "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
+  font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important;
 }
 img {
-	border-radius: 3px;
+  border-radius: 3px;
 }
-pre, code {
-	font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
-	&.raw {
-		padding: 7px 12px;
-		margin: 10px 0;
-		background-color: #f8f8f8;
-		border: 1px solid #ddd;
-		border-radius: 3px;
-		font-size: 13px;
-		line-height: 1.5;
-		overflow: auto;
-	}
-	&.wrap {
-		white-space: pre-wrap;
-		word-break: break-word;
-	}
+pre,
+code {
+  font-family: Consolas, Liberation Mono, Menlo, monospace;
+  &.raw {
+    padding: 7px 12px;
+    margin: 10px 0;
+    background-color: #f8f8f8;
+    border: 1px solid #ddd;
+    border-radius: 3px;
+    font-size: 13px;
+    line-height: 1.5;
+    overflow: auto;
+  }
+  &.wrap {
+    white-space: pre-wrap;
+    word-break: break-word;
+  }
 }
 .dont-break-out {
   /* These are technically the same, but use both */
@@ -55,396 +60,397 @@ pre, code {
   hyphens: auto;
 }
 .full.height {
-	padding: 0;
-	margin: 0 0 -@footer-margin*2 0;
-	min-height: 100%;
+  padding: 0;
+  margin: 0 0 -@footer-margin*2 0;
+  min-height: 100%;
 }
 .following.bar {
-	z-index: 900;
-	left: 0;
-	width: 100%;
-	&.light {
-		background-color: white;
-		border-bottom: 1px solid #DDDDDD;
-		box-shadow: 0 2px 3px rgba(0, 0, 0, 0.04);
-	}
-	.column .menu {
-		margin-top: 0;
-	}
-	.top.menu a.item.brand {
-		padding-left: 0;
+  z-index: 900;
+  left: 0;
+  width: 100%;
+  &.light {
+    background-color: white;
+    border-bottom: 1px solid #dddddd;
+    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.04);
+  }
+  .column .menu {
+    margin-top: 0;
+  }
+  .top.menu a.item.brand {
+    padding-left: 0;
     padding-right: 0;
-	}
-	.brand .ui.mini.image {
-		width: 30px;
-	}
-	.top.menu a.item:hover,
-	.top.menu .dropdown.item:hover,
-	.top.menu .dropdown.item.active	{
-		background-color: transparent;
-	}
-	.top.menu a.item:hover {
-		color: rgba(0,0,0,.45);
-	}
-	.top.menu .menu {
-		z-index: 900;
-	}
-	.icon,
-	.octicon {
-		margin-right: 5px !important;
-	}
-	.head.link.item {
-		padding-right: 0 !important;
-	}
-	.avatar > .ui.image {
-		margin-right: 0;
-	}
-	.avatar .octicon-triangle-down {
-		margin-top: 6.5px;
-	}
-	.searchbox {
-		background-color: rgb(244, 244, 244) !important;
-		&:focus {
-			background-color: rgb(233, 233, 233) !important;
-		}
-	}
-	.text .octicon {
-		width: 16px;
-		text-align: center;
-	}
-	.right.menu {
-		.menu {
-			left: auto;
-			right: 0;
-		}
-		.dropdown .menu {
-			margin-top: 0;
-		}
-	}
+  }
+  .brand .ui.mini.image {
+    width: 30px;
+  }
+  .top.menu a.item:hover,
+  .top.menu .dropdown.item:hover,
+  .top.menu .dropdown.item.active {
+    background-color: transparent;
+  }
+  .top.menu a.item:hover {
+    color: rgba(0, 0, 0, 0.45);
+  }
+  .top.menu .menu {
+    z-index: 900;
+  }
+  .icon,
+  .octicon {
+    margin-right: 5px !important;
+  }
+  .head.link.item {
+    padding-right: 0 !important;
+  }
+  .avatar > .ui.image {
+    margin-right: 0;
+  }
+  .avatar .octicon-triangle-down {
+    margin-top: 6.5px;
+  }
+  .searchbox {
+    background-color: rgb(244, 244, 244) !important;
+    &:focus {
+      background-color: rgb(233, 233, 233) !important;
+    }
+  }
+  .text .octicon {
+    width: 16px;
+    text-align: center;
+  }
+  .right.menu {
+    .menu {
+      left: auto;
+      right: 0;
+    }
+    .dropdown .menu {
+      margin-top: 0;
+    }
+  }
 }
 
 .ui {
-	&.left {
-		float: left;
-	}
-	&.right {
-		float: right;
-	}
-
-	&.container {
-		&.fluid {
-			&.padded {
-				padding: 0 10px 0 10px;
-			}
-		}
-	}
-
-	&.form {
-		.ui.button {
-			font-weight: normal;
-		}
-		.box.field {
-			padding-left: 27px;
-		}
-	}
-
-	&.menu,
-	&.vertical.menu,
-	&.segment {
-		box-shadow: none;
-	}
-
-	.text {
-		&.red {
-			color: #d95c5c !important;
-			a {
-				color: #d95c5c !important;
-				&:hover {
-					color: #E67777 !important;
-				}
-			}
-		}
-		&.blue {
-			color: #428bca !important;
-			a {
-				color: #15c !important;
-				&:hover {
-					color: #428bca !important;
-				}
-			}
-		}
-		&.black {
-			color: #444;
-			&:hover {
-				color: #000;
-			}
-		}
-		&.grey {
-			color: #767676 !important;
-			a {
-				color: #444 !important;
-				&:hover {
-					color: #000 !important;
-				}
-			}
-		}
-		&.light.grey {
-			color: #888 !important;
-		}
-		&.green {
-			color: #6cc644 !important;
-		}
-		&.purple {
-			color: #6e5494 !important;
-		}
-		&.yellow {
-			color: #FBBD08 !important;
-		}
-		&.gold {
-			color: #a1882b !important;
-		}
-
-		&.left {
-			text-align: left !important;
-		}
-		&.right {
-			text-align: right !important;
-		}
-		&.small {
-			font-size: 0.75em;
-		}
-		&.normal {
-			font-weight: normal;
-		}
-		&.bold {
-			font-weight: bold;
-		}
-		&.italic {
-			font-style: italic;
-		}
-
-		&.truncate {
-			overflow: hidden;
-			text-overflow: ellipsis;
-			white-space: nowrap;
-			display: inline-block;
-		}
-
-		&.thin {
-			font-weight: normal;
-		}
-
-		&.middle {
-			vertical-align: middle;
-		}
-	}
-
-	.message {
-		text-align: center;
-	}
-
-	.header > i + .content {
-		padding-left: 0.75rem;
-		vertical-align: middle;
-	}
-	.warning {
-		&.header {
-			background-color: #F9EDBE !important;
-			border-color: #F0C36D;
-		}
-		&.segment {
-			border-color: #F0C36D;
-		}
-	}
-	.info {
-		&.segment {
-			border: 1px solid #c5d5dd;
-			&.top {
-				background-color: #e6f1f6 !important;
-				h3, h4 {
-					margin-top: 0;
-				}
-				h3:last-child {
-					margin-top: 4px;
-				}
-				> :last-child {
-					margin-bottom: 0;
-				}
-			}
-		}
-	}
-
-	.normal.header {
-		font-weight: normal;
-	}
-
-	.avatar.image {
-		border-radius: 3px;
-	}
-
-	.form {
-		.fake {
-			display: none !important;
-		}
-
-		.sub.field {
-			margin-left: 25px;
-		}
-	}
-
-	.sha.label {
-		font-family: Consolas, Menlo, Monaco, "Lucida Console", monospace;
-		font-size: 13px;
-		padding: 6px 10px 4px 10px;
-		font-weight: normal;
-		margin: 0 6px;
-	}
-
-	&.status.buttons {
-		.octicon {
-			margin-right: 4px;
-		}
-	}
-
-	&.inline.delete-button {
-		padding: 8px 15px;
-		font-weight: normal;
-	}
+  &.left {
+    float: left;
+  }
+  &.right {
+    float: right;
+  }
+
+  &.container {
+    &.fluid {
+      &.padded {
+        padding: 0 10px 0 10px;
+      }
+    }
+  }
+
+  &.form {
+    .ui.button {
+      font-weight: normal;
+    }
+    .box.field {
+      padding-left: 27px;
+    }
+  }
+
+  &.menu,
+  &.vertical.menu,
+  &.segment {
+    box-shadow: none;
+  }
+
+  .text {
+    &.red {
+      color: #d95c5c !important;
+      a {
+        color: #d95c5c !important;
+        &:hover {
+          color: #e67777 !important;
+        }
+      }
+    }
+    &.blue {
+      color: #428bca !important;
+      a {
+        color: #15c !important;
+        &:hover {
+          color: #428bca !important;
+        }
+      }
+    }
+    &.black {
+      color: #444;
+      &:hover {
+        color: #000;
+      }
+    }
+    &.grey {
+      color: #767676 !important;
+      a {
+        color: #444 !important;
+        &:hover {
+          color: #000 !important;
+        }
+      }
+    }
+    &.light.grey {
+      color: #888 !important;
+    }
+    &.green {
+      color: #6cc644 !important;
+    }
+    &.purple {
+      color: #6e5494 !important;
+    }
+    &.yellow {
+      color: #fbbd08 !important;
+    }
+    &.gold {
+      color: #a1882b !important;
+    }
+
+    &.left {
+      text-align: left !important;
+    }
+    &.right {
+      text-align: right !important;
+    }
+    &.small {
+      font-size: 0.75em;
+    }
+    &.normal {
+      font-weight: normal;
+    }
+    &.bold {
+      font-weight: bold;
+    }
+    &.italic {
+      font-style: italic;
+    }
+
+    &.truncate {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      display: inline-block;
+    }
+
+    &.thin {
+      font-weight: normal;
+    }
+
+    &.middle {
+      vertical-align: middle;
+    }
+  }
+
+  .message {
+    text-align: center;
+  }
+
+  .header > i + .content {
+    padding-left: 0.75rem;
+    vertical-align: middle;
+  }
+  .warning {
+    &.header {
+      background-color: #f9edbe !important;
+      border-color: #f0c36d;
+    }
+    &.segment {
+      border-color: #f0c36d;
+    }
+  }
+  .info {
+    &.segment {
+      border: 1px solid #c5d5dd;
+      &.top {
+        background-color: #e6f1f6 !important;
+        h3,
+        h4 {
+          margin-top: 0;
+        }
+        h3:last-child {
+          margin-top: 4px;
+        }
+        > :last-child {
+          margin-bottom: 0;
+        }
+      }
+    }
+  }
+
+  .normal.header {
+    font-weight: normal;
+  }
+
+  .avatar.image {
+    border-radius: 3px;
+  }
+
+  .form {
+    .fake {
+      display: none !important;
+    }
+
+    .sub.field {
+      margin-left: 25px;
+    }
+  }
+
+  .sha.label {
+    font-family: Consolas, Liberation Mono, Menlo, monospace;
+    font-size: 13px;
+    padding: 6px 10px 4px 10px;
+    font-weight: normal;
+    margin: 0 6px;
+  }
+
+  &.status.buttons {
+    .octicon {
+      margin-right: 4px;
+    }
+  }
+
+  &.inline.delete-button {
+    padding: 8px 15px;
+    font-weight: normal;
+  }
 }
 
 .overflow.menu {
-	.items {
-		max-height: 300px;
-		overflow-y: auto;
-		.item {
-			position: relative;
-			cursor: pointer;
-			display: block;
-			border: none;
-			height: auto;
-			border-top: none;
-			line-height: 1em;
-			color: rgba(0,0,0,.8);
-			padding: .71428571em 1.14285714em !important;
-			font-size: 1rem;
-			text-transform: none;
-			font-weight: 400;
-			box-shadow: none;
-			-webkit-touch-callout: none;
-			&.active {
-				font-weight: 700;
-			}
-			&:hover {
-				background: rgba(0,0,0,.05);
-				color: rgba(0,0,0,.8);
-				z-index: 13;
-			}
-		}
-	}
+  .items {
+    max-height: 300px;
+    overflow-y: auto;
+    .item {
+      position: relative;
+      cursor: pointer;
+      display: block;
+      border: none;
+      height: auto;
+      border-top: none;
+      line-height: 1em;
+      color: rgba(0, 0, 0, 0.8);
+      padding: 0.71428571em 1.14285714em !important;
+      font-size: 1rem;
+      text-transform: none;
+      font-weight: 400;
+      box-shadow: none;
+      -webkit-touch-callout: none;
+      &.active {
+        font-weight: 700;
+      }
+      &:hover {
+        background: rgba(0, 0, 0, 0.05);
+        color: rgba(0, 0, 0, 0.8);
+        z-index: 13;
+      }
+    }
+  }
 }
 
 .scrolling.menu {
-	.item.selected {
-		font-weight: 700 !important;
-	}
+  .item.selected {
+    font-weight: 700 !important;
+  }
 }
 
 footer {
-	margin-top: @footer-margin+14px !important;
-	height: @footer-margin;
-	background-color: white;
-	border-top: 1px solid #d6d6d6;
-	clear: both;
-	width: 100%;
-	color: #888888;
-	.container {
-		padding-top: 10px;
-		.fa {
-			width: 16px;
-			text-align: center;
-			color: #428bca;
-		}
-		.links >* {
-			border-left: 1px solid #d6d6d6;
-			padding-left: 8px;
-			margin-left: 5px;
-			&:first-child {
-				border-left: none;
-			}
-		}
-	}
-
-	.ui.language .menu {
-		max-height: 500px;
-		overflow-y: auto;
-		margin-bottom: 7px;
-	}
+  margin-top: @footer-margin+14px !important;
+  height: @footer-margin;
+  background-color: white;
+  border-top: 1px solid #d6d6d6;
+  clear: both;
+  width: 100%;
+  color: #888888;
+  .container {
+    padding-top: 10px;
+    .fa {
+      width: 16px;
+      text-align: center;
+      color: #428bca;
+    }
+    .links > * {
+      border-left: 1px solid #d6d6d6;
+      padding-left: 8px;
+      margin-left: 5px;
+      &:first-child {
+        border-left: none;
+      }
+    }
+  }
+
+  .ui.language .menu {
+    max-height: 500px;
+    overflow-y: auto;
+    margin-bottom: 7px;
+  }
 }
 
 .hide {
-	display: none;
+  display: none;
 }
 .display {
-	&.inline {
-		display: inline;
-	}
+  &.inline {
+    display: inline;
+  }
 }
 .center {
-	text-align: center;
+  text-align: center;
 }
 
 .no-padding-left {
-	padding-left: 0 !important;
+  padding-left: 0 !important;
 }
 
 .generate-img(16);
 .generate-img(@n, @i: 1) when (@i =< @n) {
-	.img-@{i} {
-		width: (2px * @i) !important;
-		height: (2px * @i) !important;
-	}
-	.generate-img(@n, (@i + 1));
+  .img-@{i} {
+    width: (2px * @i) !important;
+    height: (2px * @i) !important;
+  }
+  .generate-img(@n, (@i + 1));
 }
 
 // Accessibility
 .sr-only {
-	position: absolute;
-	width: 1px;
-	height: 1px;
-	padding: 0;
-	margin: -1px;
-	overflow: hidden;
-	clip: rect(0, 0, 0, 0);
-	border: 0;
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0, 0, 0, 0);
+  border: 0;
 }
 .sr-only-focusable:active,
 .sr-only-focusable:focus {
-	position: static;
-	width: auto;
-	height: auto;
-	margin: 0;
-	overflow: visible;
-	clip: auto;
+  position: static;
+  width: auto;
+  height: auto;
+  margin: 0;
+  overflow: visible;
+  clip: auto;
 }
 
 @media only screen and (max-width: 991px) and (min-width: 768px) {
-	.ui.container {
-		width: 95%;
-	}
+  .ui.container {
+    width: 95%;
+  }
 }
 
 /* Overrides some styles of the Highlight.js plugin */
 .hljs {
-	background: inherit !important;
-	padding: 0 !important;
+  background: inherit !important;
+  padding: 0 !important;
 }
 
 // Reset CSS to prevent UI breaks
-.ui.dropdown .menu>.item>.image,
-.ui.dropdown .menu>.item>img,
-.ui.dropdown>.text>.image,
-.ui.dropdown>.text>img {
-	vertical-align: middle;
-	margin-top: 0;
-    margin-bottom: 0;
+.ui.dropdown .menu > .item > .image,
+.ui.dropdown .menu > .item > img,
+.ui.dropdown > .text > .image,
+.ui.dropdown > .text > img {
+  vertical-align: middle;
+  margin-top: 0;
+  margin-bottom: 0;
 }

Fichier diff supprimé car celui-ci est trop grand
+ 948 - 949
public/less/_repository.less


+ 3 - 3
templates/admin/config.tmpl

@@ -458,11 +458,11 @@
 						<dt>{{.i18n.Tr "admin.config.git.disable_diff_highlight"}}</dt>
 						<dd><i class="fa fa{{if .Git.DisableDiffHighlight}}-check{{end}}-square-o"></i></dd>
 						<dt>{{.i18n.Tr "admin.config.git.max_diff_lines"}}</dt>
-						<dd>{{.Git.MaxGitDiffLines}}</dd>
+						<dd>{{.Git.MaxDiffLines}}</dd>
 						<dt>{{.i18n.Tr "admin.config.git.max_diff_line_characters"}}</dt>
-						<dd>{{.Git.MaxGitDiffLineCharacters}}</dd>
+						<dd>{{.Git.MaxDiffLineChars}}</dd>
 						<dt>{{.i18n.Tr "admin.config.git.max_diff_files"}}</dt>
-						<dd>{{.Git.MaxGitDiffFiles}}</dd>
+						<dd>{{.Git.MaxDiffFiles}}</dd>
 						<dt>{{.i18n.Tr "admin.config.git.gc_args"}}</dt>
 						<dd><code>{{.Git.GCArgs}}</code></dd>
 

+ 2 - 2
templates/base/head.tmpl

@@ -60,7 +60,7 @@
 
 	<!-- Stylesheet -->
 	<link rel="stylesheet" href="{{AppSubURL}}/css/semantic-2.4.2.min.css">
-	<link rel="stylesheet" href="{{AppSubURL}}/css/gogs.css?v={{MD5 AppVer}}">
+	<link rel="stylesheet" href="{{AppSubURL}}/css/gogs.css?v={{BuildCommit}}">
 	<noscript>
 		<style>
 			.dropdown:hover > .menu { display: block; }
@@ -70,7 +70,7 @@
 
 	<!-- JavaScript -->
 	<script src="{{AppSubURL}}/js/semantic-2.4.2.min.js"></script>
-	<script src="{{AppSubURL}}/js/gogs.js?v={{MD5 AppVer}}"></script>
+	<script src="{{AppSubURL}}/js/gogs.js?v={{BuildCommit}}"></script>
 
 	<title>{{if .Title}}{{.Title}} - {{end}}{{AppName}}</title>
 

+ 2 - 3
templates/repo/commits_table.tmpl

@@ -29,8 +29,7 @@
 				</tr>
 			</thead>
 			<tbody>
-				{{ $r:= List .Commits}}
-				{{range $r}}
+				{{range .Commits}}
 					<tr>
 						<td class="author">
 							{{if .User}}
@@ -47,7 +46,7 @@
 							{{else}}
 								<a rel="nofollow" class="ui sha label" href="{{AppSubURL}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">{{ShortSHA1 .ID.String}}</a>
 							{{end}}
-							<span class="{{if gt .ParentCount 1}}grey text {{end}} has-emoji">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas | Str2HTML}}</span>
+							<span class="{{if gt .ParentsCount 1}}grey text {{end}} has-emoji">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas | Str2HTML}}</span>
 						</td>
 						<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>
 					</tr>

+ 17 - 17
templates/repo/diff/box.tmpl

@@ -4,7 +4,7 @@
 	<div class="diff-detail-box diff-box">
 		<div>
 			<i class="fa fa-retweet"></i>
-			{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAddition .Diff.TotalDeletion | Str2HTML}}
+			{{.i18n.Tr "repo.diff.stats_desc" .Diff.NumFiles .Diff.TotalAdditions .Diff.TotalDeletions | Str2HTML}}
 			<div class="ui right">
 				<a class="ui tiny basic toggle button" href="?style={{if .IsSplitStyle}}unified{{else}}split{{end}}">{{ if .IsSplitStyle }}{{.i18n.Tr "repo.diff.show_unified_view"}}{{else}}{{.i18n.Tr "repo.diff.show_split_view"}}{{end}}</a>
 				<a class="ui tiny basic toggle button" data-target="#diff-files">{{.i18n.Tr "repo.diff.show_diff_stats"}}</a>
@@ -14,19 +14,19 @@
 			{{range .Diff.Files}}
 				<li>
 					<div class="diff-counter count pull-right">
-						{{if not .IsBin}}
-							<span class="add" data-line="{{.Addition}}">{{.Addition}}</span>
+						{{if not .IsBinary}}
+							<span class="add" data-line="{{.NumAdditions}}">{{.NumAdditions}}</span>
 							<span class="bar">
 								<span class="pull-left add"></span>
 								<span class="pull-left del"></span>
 							</span>
-							<span class="del" data-line="{{.Deletion}}">{{.Deletion}}</span>
+							<span class="del" data-line="{{.NumDeletions}}">{{.NumDeletions}}</span>
 						{{else}}
 							<span>{{$.i18n.Tr "repo.diff.bin"}}</span>
 						{{end}}
 					</div>
 					<!-- todo finish all file status, now modify, add, delete and rename -->
-					<span class="status {{DiffTypeToStr .GetType}} poping up" data-content="{{DiffTypeToStr .GetType}}" data-variation="inverted tiny" data-position="right center">&nbsp;</span>
+					<span class="status {{DiffFileTypeToStr .Type}} poping up" data-content="{{DiffFileTypeToStr .Type}}" data-variation="inverted tiny" data-position="right center">&nbsp;</span>
 					<a class="file" href="#diff-{{.Index}}">{{.Name}}</a>
 				</li>
 			{{end}}
@@ -40,12 +40,12 @@
 					{{$.i18n.Tr "repo.diff.file_suppressed"}}
 					<div class="diff-counter count ui left">
 						{{if not $file.IsRenamed}}
-							<span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span>
+							<span class="add" data-line="{{.NumAdditions}}">+ {{.NumAdditions}}</span>
 							<span class="bar">
 								<span class="pull-left add"></span>
 								<span class="pull-left del"></span>
 							</span>
-							<span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span>
+							<span class="del" data-line="{{.NumDeletions}}">- {{.NumDeletions}}</span>
 						{{end}}
 					</div>
 					<span class="file">{{$file.Name}}</span>
@@ -55,15 +55,15 @@
 			<div class="diff-file-box diff-box file-content {{TabSizeClass $.Editorconfig $file.Name}}" id="diff-{{.Index}}">
 				<h4 class="ui top attached normal header">
 					<div class="diff-counter count ui left">
-						{{if $file.IsBin}}
+						{{if $file.IsBinary}}
 							{{$.i18n.Tr "repo.diff.bin"}}
 						{{else if not $file.IsRenamed}}
-							<span class="add" data-line="{{.Addition}}">+ {{.Addition}}</span>
+							<span class="add" data-line="{{.NumAdditions}}">+ {{.NumAdditions}}</span>
 							<span class="bar">
 								<span class="pull-left add"></span>
 								<span class="pull-left del"></span>
 							</span>
-							<span class="del" data-line="{{.Deletion}}">- {{.Deletion}}</span>
+							<span class="del" data-line="{{.NumDeletions}}">- {{.NumDeletions}}</span>
 						{{end}}
 					</div>
 					<span class="file">{{if $file.IsRenamed}}{{$file.OldName}} &rarr; {{end}}{{$file.Name}}</span>
@@ -80,7 +80,7 @@
 				<div class="ui unstackable attached table segment">
 					{{if not $file.IsRenamed}}
 						{{$isImage := (call $.IsImageFile $file.Name)}}
-						{{if and $isImage}}
+						{{if $isImage}}
 							<div class="center">
 								<img src="{{$.RawPath}}/{{EscapePound .Name}}">
 							</div>
@@ -92,22 +92,22 @@
 											{{$highlightClass := $file.HighlightClass}}
 											{{range $j, $section := $file.Sections}}
 												{{range $k, $line := $section.Lines}}
-													<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}">
-														{{if eq .GetType 4}}
+													<tr class="{{DiffLineTypeToStr .Type}}-code nl-{{$k}} ol-{{$k}}">
+														{{if eq .Type 4}}
 															<td class="lines-num"></td>
 															<td colspan="3"  class="lines-code">
 																<pre><code class="{{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{$section.ComputedInlineDiffFor $line}}</code></pre>
 															</td>
 														{{else}}
-															<td class="lines-num lines-num-old" {{if $line.LeftIdx}} id="diff-{{Sha1 $file.Index}}L{{$line.LeftIdx}}" data-line-number="{{$line.LeftIdx}}"{{end}}>
+															<td class="lines-num lines-num-old" {{if $line.LeftLine}} id="diff-{{Sha1 $file.Index}}L{{$line.LeftLine}}" data-line-number="{{$line.LeftLine}}"{{end}}>
 															</td>
 															<td class="lines-code halfwidth">
-																<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.LeftIdx}}{{$section.ComputedInlineDiffFor $line}}{{end}}</code></pre>
+																<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.LeftLine}}{{$section.ComputedInlineDiffFor $line}}{{end}}</code></pre>
 															</td>
-															<td class="lines-num lines-num-new" {{if $line.RightIdx}} id="diff-{{Sha1 $file.Index}}R{{$line.RightIdx}}" data-line-number="{{$line.RightIdx}}"{{end}}>
+															<td class="lines-num lines-num-new" {{if $line.RightLine}} id="diff-{{Sha1 $file.Index}}R{{$line.RightLine}}" data-line-number="{{$line.RightLine}}"{{end}}>
 															</td>
 															<td class="lines-code halfwidth">
-																<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.RightIdx}}{{$section.ComputedInlineDiffFor $line}}{{end}}</code></pre>
+																<pre><code class="wrap {{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{if $line.RightLine}}{{$section.ComputedInlineDiffFor $line}}{{end}}</code></pre>
 															</td>
 														{{end}}
 													</tr>

+ 5 - 5
templates/repo/diff/section_unified.tmpl

@@ -2,18 +2,18 @@
 {{$highlightClass := $file.HighlightClass}}
 {{range $j, $section := $file.Sections}}
 	{{range $k, $line := $section.Lines}}
-		<tr class="{{DiffLineTypeToStr .GetType}}-code nl-{{$k}} ol-{{$k}}">
-			{{if eq .GetType 4}}
+		<tr class="{{DiffLineTypeToStr .Type}}-code nl-{{$k}} ol-{{$k}}">
+			{{if eq .Type 4}}
 				<td colspan="2" class="lines-num">
 					{{/* {{if gt $j 0}}<span class="fold octicon octicon-fold"></span>{{end}} */}}
 				</td>
 			{{else}}
-				<td class="lines-num lines-num-old" {{if $line.LeftIdx}} id="diff-{{$file.Index}}L{{$line.LeftIdx}}" data-line-number="{{$line.LeftIdx}}"{{end}}></td>
-				<td class="lines-num lines-num-new" {{if $line.RightIdx}} id="diff-{{$file.Index}}R{{$line.RightIdx}}" data-line-number="{{$line.RightIdx}}"{{end}}></td>
+				<td class="lines-num lines-num-old" {{if $line.LeftLine}} id="diff-{{$file.Index}}L{{$line.LeftLine}}" data-line-number="{{$line.LeftLine}}"{{end}}></td>
+				<td class="lines-num lines-num-new" {{if $line.RightLine}} id="diff-{{$file.Index}}R{{$line.RightLine}}" data-line-number="{{$line.RightLine}}"{{end}}></td>
 			{{end}}
 			<td class="lines-code">
 				<pre><code class="{{if $highlightClass}}language-{{$highlightClass}}{{else}}nohighlight{{end}}">{{$section.ComputedInlineDiffFor $line}}</code></pre>
 			</td>
 		</tr>
 	{{end}}
-{{end}}
+{{end}}

+ 1 - 1
templates/repo/settings/githook_edit.tmpl

@@ -20,7 +20,7 @@
 							</div>
 							<div class="field">
 								<label for="content">{{$.i18n.Tr "repo.settings.githook_content"}}</label>
-								<textarea id="content" name="content" wrap="off" autofocus>{{if .IsActive}}{{.Content}}{{else}}{{.Sample}}{{end}}</textarea>
+								<textarea id="content" name="content" wrap="off" autofocus>{{.Content}}</textarea>
 							</div>
 
 							<div class="inline field">

+ 1 - 1
templates/repo/settings/githooks.tmpl

@@ -16,7 +16,7 @@
 						</div>
 						{{range .Hooks}}
 							<div class="item">
-								<span class="text {{if .IsActive}}green{{else}}grey{{end}}"><i class="octicon octicon-primitive-dot"></i></span>
+								<span class="text {{if not .IsSample}}green{{else}}grey{{end}}"><i class="octicon octicon-primitive-dot"></i></span>
 								<span>{{.Name}}</span>
 								<a class="text blue ui right" href="{{$.RepoLink}}/settings/hooks/git/{{.Name}}"><i class="fa fa-pencil"></i></a>
 							</div>

+ 1 - 1
templates/repo/settings/options.tmpl

@@ -79,7 +79,7 @@
 							</div>
 							<div class="field">
 								<label for="mirror_address">{{.i18n.Tr "repo.mirror_address"}}</label>
-								<input id="mirror_address" name="mirror_address" value="{{.Mirror.FullAddress}}" required>
+								<input id="mirror_address" name="mirror_address" value="{{.Mirror.RawAddress}}" required>
 								<p class="help">{{.i18n.Tr "repo.mirror_address_desc"}}</p>
 							</div>
 

+ 9 - 16
templates/repo/view_list.tmpl

@@ -23,35 +23,28 @@
 				<td colspan="3"><i class="octicon octicon-mail-reply"></i><a href="{{EscapePound .BranchLink}}{{.ParentPath}}">..</a></td>
 			</tr>
 		{{end}}
-		{{range $item := .Files}}
-			{{$entry := index $item 0}}
-			{{$commit := index $item 1}}
+		{{range .Files}}
 			<tr>
-				{{if $entry.IsSubModule}}
+				{{if .Submodule}}
 					<td>
 						<span class="octicon octicon-file-submodule"></span>
-						{{$refURL := $commit.RefURL AppURL $.BranchLink}}
-						{{if $refURL}}
-							<a href="{{$refURL}}">{{$entry.Name}}</a> @ <a href="{{$refURL}}/commit/{{$commit.RefID}}">{{ShortSHA1 $commit.RefID}}</a>
-						{{else}}
-							{{$entry.Name}} @ {{ShortSHA1 $commit.RefID}}
-						{{end}}
+						<a href="{{InferSubmoduleURL .Submodule}}">{{.Entry.Name}} @ {{ShortSHA1 .Submodule.Commit}}</a>
 					</td>
 				{{else}}
 					<td class="name">
-						{{if $entry.IsLink}}
+						{{if .Entry.IsSymlink}}
 							<span class="octicon octicon-file-symlink-file"></span>
 						{{else}}
-							<span class="octicon octicon-file-{{if or $entry.IsDir}}directory{{else}}text{{end}}"></span>
+							<span class="octicon octicon-file-{{if or .Entry.IsTree}}directory{{else}}text{{end}}"></span>
 						{{end}}
-						<a href="{{EscapePound $.TreeLink}}/{{EscapePound $entry.Name}}">{{$entry.Name}}</a>
+						<a href="{{EscapePound $.TreeLink}}/{{EscapePound .Entry.Name}}">{{.Entry.Name}}</a>
 					</td>
 				{{end}}
 				<td class="message collapsing has-emoji">
-					<a rel="nofollow" class="ui sha label" href="{{$.RepoLink}}/commit/{{$commit.ID}}">{{ShortSHA1 $commit.ID.String}}</a>
-					{{RenderCommitMessage false $commit.Summary $.RepoLink $.Repository.ComposeMetas | Str2HTML}}
+					<a rel="nofollow" class="ui sha label" href="{{$.RepoLink}}/commit/{{.Commit.ID}}">{{ShortSHA1 .Commit.ID.String}}</a>
+					{{RenderCommitMessage false .Commit.Summary $.RepoLink $.Repository.ComposeMetas | Str2HTML}}
 				</td>
-				<td class="text grey right age">{{TimeSince $commit.Committer.When $.Lang}}</td>
+				<td class="text grey right age">{{TimeSince .Commit.Committer.When $.Lang}}</td>
 			</tr>
 		{{end}}
 	</tbody>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff