d8a994ef24",
- "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", " d8a994ef24",
- "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2",
- "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae", "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae",
- }
+ htmlFlags := 0
+ htmlFlags |= blackfriday.HTML_SKIP_STYLE
+ htmlFlags |= blackfriday.HTML_OMIT_CONTENTS
+ renderer := &MarkdownRenderer{
+ Renderer: blackfriday.HtmlRenderer(htmlFlags, "", ""),
+ }
- for i := 0; i < len(testCases); i += 2 {
- renderer.AutoLink(buffer, []byte(testCases[i]), blackfriday.LINK_TYPE_NORMAL)
+ tests := []struct {
+ input string
+ expVal string
+ }{
+ // Issue URL
+ {input: "http://localhost:3000/user/repo/issues/3333", expVal: "#3333"},
+ {input: "http://1111/2222/ssss-issues/3333?param=blah&blahh=333", expVal: "http://1111/2222/ssss-issues/3333?param=blah&blahh=333"},
+ {input: "http://test.com/issues/33333", expVal: "http://test.com/issues/33333"},
+ {input: "http://test.com/issues/3", expVal: "http://test.com/issues/3"},
+ {input: "http://issues/333", expVal: "http://issues/333"},
+ {input: "https://issues/333", expVal: "https://issues/333"},
+ {input: "http://tissues/0", expVal: "http://tissues/0"},
- line, _ := buffer.ReadString(0)
- So(line, ShouldEqual, testCases[i+1])
- }
- })
+ // Commit URL
+ {input: "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae", expVal: " d8a994ef24"},
+ {input: "http://localhost:3000/user/project/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", expVal: " d8a994ef24"},
+ {input: "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2", expVal: "https://external-link.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"},
+ {input: "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae", expVal: "https://commit/d8a994ef243349f321568f9e36d5c3f444b99cae"},
+ }
+ for _, test := range tests {
+ t.Run("", func(t *testing.T) {
+ buf := new(bytes.Buffer)
+ renderer.AutoLink(buf, []byte(test.input), blackfriday.LINK_TYPE_NORMAL)
+ assert.Equal(t, test.expVal, buf.String())
})
- })
+ }
}
diff --git a/internal/markup/markup_test.go b/internal/markup/markup_test.go
index be19047bc..911a597c0 100644
--- a/internal/markup/markup_test.go
+++ b/internal/markup/markup_test.go
@@ -5,306 +5,213 @@
package markup_test
import (
- "strings"
"testing"
- . "github.com/smartystreets/goconvey/convey"
+ "github.com/stretchr/testify/assert"
- "gogs.io/gogs/internal/conf"
. "gogs.io/gogs/internal/markup"
)
func Test_IsReadmeFile(t *testing.T) {
- Convey("Detect README file extension", t, func() {
- testCases := []struct {
- ext string
- match bool
- }{
- {"readme", true},
- {"README", true},
- {"readme.md", true},
- {"readme.markdown", true},
- {"readme.mdown", true},
- {"readme.mkd", true},
- {"readme.org", true},
- {"readme.rst", true},
- {"readme.asciidoc", true},
- {"readme_ZH", true},
- }
-
- for _, tc := range testCases {
- So(IsReadmeFile(tc.ext), ShouldEqual, tc.match)
- }
- })
+ tests := []struct {
+ name string
+ expVal bool
+ }{
+ {name: "readme", expVal: true},
+ {name: "README", expVal: true},
+ {name: "readme.md", expVal: true},
+ {name: "readme.markdown", expVal: true},
+ {name: "readme.mdown", expVal: true},
+ {name: "readme.mkd", expVal: true},
+ {name: "readme.org", expVal: true},
+ {name: "readme.rst", expVal: true},
+ {name: "readme.asciidoc", expVal: true},
+ {name: "readme_ZH", expVal: true},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expVal, IsReadmeFile(test.name))
+ })
+ }
}
func Test_FindAllMentions(t *testing.T) {
- Convey("Find all mention patterns", t, func() {
- testCases := []struct {
- content string
- matches string
- }{
- {"@Unknwon, what do you think?", "Unknwon"},
- {"@Unknwon what do you think?", "Unknwon"},
- {"Hi @Unknwon, sounds good to me", "Unknwon"},
- {"cc/ @Unknwon @User", "Unknwon,User"},
- }
-
- for _, tc := range testCases {
- So(strings.Join(FindAllMentions(tc.content), ","), ShouldEqual, tc.matches)
- }
- })
+ tests := []struct {
+ input string
+ expMatches []string
+ }{
+ {input: "@unknwon, what do you think?", expMatches: []string{"unknwon"}},
+ {input: "@unknwon what do you think?", expMatches: []string{"unknwon"}},
+ {input: "Hi @unknwon, sounds good to me", expMatches: []string{"unknwon"}},
+ {input: "cc/ @unknwon @eddycjy", expMatches: []string{"unknwon", "eddycjy"}},
+ }
+ for _, test := range tests {
+ t.Run("", func(t *testing.T) {
+ assert.Equal(t, test.expMatches, FindAllMentions(test.input))
+ })
+ }
}
func Test_RenderIssueIndexPattern(t *testing.T) {
- Convey("Rendering an issue reference", t, func() {
- var (
- urlPrefix = "/prefix"
- metas map[string]string = nil
- )
- conf.Server.SubpathDepth = 0
-
- Convey("To the internal issue tracker", func() {
- Convey("It should not render anything when there are no mentions", func() {
- testCases := []string{
- "",
- "this is a test",
- "test 123 123 1234",
- "#",
- "# # #",
- "# 123",
- "#abcd",
- "##1234",
- "test#1234",
- "#1234test",
- " test #1234test",
- }
-
- for i := 0; i < len(testCases); i++ {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i])
- }
- })
- Convey("It should render freestanding mentions", func() {
- testCases := []string{
- "#1234 test", "#1234 test",
- "test #1234 issue", "test #1234 issue",
- "test issue #1234", "test issue #1234",
- "#5 test", "#5 test",
- "test #5 issue", "test #5 issue",
- "test issue #5", "test issue #5",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should not render issue mention without leading space", func() {
- input := []byte("test#54321 issue")
- expected := "test#54321 issue"
- So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected)
- })
- Convey("It should not render issue mention without trailing space", func() {
- input := []byte("test #54321issue")
- expected := "test #54321issue"
- So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected)
- })
- Convey("It should render issue mention in parentheses", func() {
- testCases := []string{
- "(#54321 issue)", "(#54321 issue)",
- "test (#54321) issue", "test (#54321) issue",
- "test (#54321 extra) issue", "test (#54321 extra) issue",
- "test (#54321 issue)", "test (#54321 issue)",
- "test (#54321)", "test (#54321)",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should render issue mention in square brackets", func() {
- testCases := []string{
- "[#54321 issue]", "[#54321 issue]",
- "test [#54321] issue", "test [#54321] issue",
- "test [#54321 extra] issue", "test [#54321 extra] issue",
- "test [#54321 issue]", "test [#54321 issue]",
- "test [#54321]", "test [#54321]",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should render multiple issue mentions in the same line", func() {
- testCases := []string{
- "#54321 #1243", "#54321 #1243",
- "test #54321 #1243", "test #54321 #1243",
- "(#54321 #1243)", "(#54321 #1243)",
- "(#54321)(#1243)", "(#54321)(#1243)",
- "text #54321 test #1243 issue", "text #54321 test #1243 issue",
- "#1 (#4321) test", "#1 (#4321) test",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- })
- Convey("To an external issue tracker with numeric style", func() {
- metas = make(map[string]string)
- metas["format"] = "https://someurl.com/{user}/{repo}/{index}"
- metas["user"] = "someuser"
- metas["repo"] = "somerepo"
- metas["style"] = ISSUE_NAME_STYLE_NUMERIC
-
- Convey("should not render anything when there are no mentions", func() {
- testCases := []string{
- "this is a test",
- "test 123 123 1234",
- "#",
- "# # #",
- "# 123",
- "#abcd",
- }
-
- for i := 0; i < len(testCases); i++ {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i])
- }
- })
- Convey("It should render freestanding issue mentions", func() {
- testCases := []string{
- "#1234 test", "#1234 test",
- "test #1234 issue", "test #1234 issue",
- "test issue #1234", "test issue #1234",
- "#5 test", "#5 test",
- "test #5 issue", "test #5 issue",
- "test issue #5", "test issue #5",
- }
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should not render issue mention without leading space", func() {
- input := []byte("test#54321 issue")
- expected := "test#54321 issue"
- So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected)
- })
- Convey("It should not render issue mention without trailing space", func() {
- input := []byte("test #54321issue")
- expected := "test #54321issue"
- So(string(RenderIssueIndexPattern(input, urlPrefix, metas)), ShouldEqual, expected)
- })
- Convey("It should render issue mention in parentheses", func() {
- testCases := []string{
- "(#54321 issue)", "(#54321 issue)",
- "test (#54321) issue", "test (#54321) issue",
- "test (#54321 extra) issue", "test (#54321 extra) issue",
- "test (#54321 issue)", "test (#54321 issue)",
- "test (#54321)", "test (#54321)",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
+ urlPrefix := "/prefix"
+ t.Run("render to internal issue tracker", func(t *testing.T) {
+ tests := []struct {
+ input string
+ expVal string
+ }{
+ {input: "", expVal: ""},
+ {input: "this is a test", expVal: "this is a test"},
+ {input: "test 123 123 1234", expVal: "test 123 123 1234"},
+ {input: "#", expVal: "#"},
+ {input: "# # #", expVal: "# # #"},
+ {input: "# 123", expVal: "# 123"},
+ {input: "#abcd", expVal: "#abcd"},
+ {input: "##1234", expVal: "##1234"},
+ {input: "test#1234", expVal: "test#1234"},
+ {input: "#1234test", expVal: "#1234test"},
+ {input: " test #1234test", expVal: " test #1234test"},
+
+ {input: "#1234 test", expVal: "#1234 test"},
+ {input: "test #1234 issue", expVal: "test #1234 issue"},
+ {input: "test issue #1234", expVal: "test issue #1234"},
+ {input: "#5 test", expVal: "#5 test"},
+ {input: "test #5 issue", expVal: "test #5 issue"},
+ {input: "test issue #5", expVal: "test issue #5"},
+
+ {input: "(#54321 issue)", expVal: "(#54321 issue)"},
+ {input: "test (#54321) issue", expVal: "test (#54321) issue"},
+ {input: "test (#54321 extra) issue", expVal: "test (#54321 extra) issue"},
+ {input: "test (#54321 issue)", expVal: "test (#54321 issue)"},
+ {input: "test (#54321)", expVal: "test (#54321)"},
+
+ {input: "[#54321 issue]", expVal: "[#54321 issue]"},
+ {input: "test [#54321] issue", expVal: "test [#54321] issue"},
+ {input: "test [#54321 extra] issue", expVal: "test [#54321 extra] issue"},
+ {input: "test [#54321 issue]", expVal: "test [#54321 issue]"},
+ {input: "test [#54321]", expVal: "test [#54321]"},
+
+ {input: "#54321 #1243", expVal: "#54321 #1243"},
+ {input: "test #54321 #1243", expVal: "test #54321 #1243"},
+ {input: "(#54321 #1243)", expVal: "(#54321 #1243)"},
+ {input: "(#54321)(#1243)", expVal: "(#54321)(#1243)"},
+ {input: "text #54321 test #1243 issue", expVal: "text #54321 test #1243 issue"},
+ {input: "#1 (#4321) test", expVal: "#1 (#4321) test"},
+ }
+ for _, test := range tests {
+ t.Run(test.input, func(t *testing.T) {
+ assert.Equal(t, test.expVal, string(RenderIssueIndexPattern([]byte(test.input), urlPrefix, nil)))
})
- Convey("It should render multiple issue mentions in the same line", func() {
- testCases := []string{
- "#54321 #1243", "#54321 #1243",
- "test #54321 #1243", "test #54321 #1243",
- "(#54321 #1243)", "(#54321 #1243)",
- "(#54321)(#1243)", "(#54321)(#1243)",
- "text #54321 test #1243 issue", "text #54321 test #1243 issue",
- "#1 (#4321) test", "#1 (#4321) test",
- }
+ }
+ })
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
+ t.Run("render to external issue tracker", func(t *testing.T) {
+ t.Run("numeric style", func(t *testing.T) {
+ metas := map[string]string{
+ "format": "https://someurl.com/{user}/{repo}/{index}",
+ "user": "someuser",
+ "repo": "somerepo",
+ "style": ISSUE_NAME_STYLE_NUMERIC,
+ }
+
+ tests := []struct {
+ input string
+ expVal string
+ }{
+ {input: "this is a test", expVal: "this is a test"},
+ {input: "test 123 123 1234", expVal: "test 123 123 1234"},
+ {input: "#", expVal: "#"},
+ {input: "# # #", expVal: "# # #"},
+ {input: "# 123", expVal: "# 123"},
+ {input: "#abcd", expVal: "#abcd"},
+
+ {input: "#1234 test", expVal: "#1234 test"},
+ {input: "test #1234 issue", expVal: "test #1234 issue"},
+ {input: "test issue #1234", expVal: "test issue #1234"},
+ {input: "#5 test", expVal: "#5 test"},
+ {input: "test #5 issue", expVal: "test #5 issue"},
+ {input: "test issue #5", expVal: "test issue #5"},
+
+ {input: "(#54321 issue)", expVal: "(#54321 issue)"},
+ {input: "test (#54321) issue", expVal: "test (#54321) issue"},
+ {input: "test (#54321 extra) issue", expVal: "test (#54321 extra) issue"},
+ {input: "test (#54321 issue)", expVal: "test (#54321 issue)"},
+ {input: "test (#54321)", expVal: "test (#54321)"},
+
+ {input: "#54321 #1243", expVal: "#54321 #1243"},
+ {input: "test #54321 #1243", expVal: "test #54321 #1243"},
+ {input: "(#54321 #1243)", expVal: "(#54321 #1243)"},
+ {input: "(#54321)(#1243)", expVal: "(#54321)(#1243)"},
+ {input: "text #54321 test #1243 issue", expVal: "text #54321 test #1243 issue"},
+ {input: "#1 (#4321) test", expVal: "#1 (#4321) test"},
+ }
+ for _, test := range tests {
+ t.Run(test.input, func(t *testing.T) {
+ assert.Equal(t, test.expVal, string(RenderIssueIndexPattern([]byte(test.input), urlPrefix, metas)))
+ })
+ }
})
- Convey("To an external issue tracker with alphanumeric style", func() {
- metas = make(map[string]string)
- metas["format"] = "https://someurl.com/{user}/{repo}/?b={index}"
- metas["user"] = "someuser"
- metas["repo"] = "somerepo"
- metas["style"] = ISSUE_NAME_STYLE_ALPHANUMERIC
- Convey("It should not render anything when there are no mentions", func() {
- testCases := []string{
- "",
- "this is a test",
- "test 123 123 1234",
- "#",
- "##1234",
- "# 123",
- "#abcd",
- "test #123",
- "abc-1234", // issue prefix must be capital
- "ABc-1234", // issue prefix must be _all_ capital
- "ABCDEFGHIJK-1234", // the limit is 10 characters in the prefix
- "ABC1234", // dash is required
- "test ABC- test", // number is required
- "test -1234 test", // prefix is required
- "testABC-123 test", // leading space is required
- "test ABC-123test", // trailing space is required
- "ABC-0123", // no leading zero
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i])
- }
- })
- Convey("It should render freestanding issue mention", func() {
- testCases := []string{
- "OTT-1234 test", "OTT-1234 test",
- "test T-12 issue", "test T-12 issue",
- "test issue ABCDEFGHIJ-1234567890", "test issue ABCDEFGHIJ-1234567890",
- "A-1 test", "A-1 test",
- "test ZED-1 issue", "test ZED-1 issue",
- "test issue DEED-7154", "test issue DEED-7154",
- }
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should render issue mention in parentheses", func() {
- testCases := []string{
- "(ABG-124 issue)", "(ABG-124 issue)",
- "test (ABG-124) issue", "test (ABG-124) issue",
- "test (ABG-124 extra) issue", "test (ABG-124 extra) issue",
- "test (ABG-124 issue)", "test (ABG-124 issue)",
- "test (ABG-124)", "test (ABG-124)",
- }
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should render issue mention in square brackets", func() {
- testCases := []string{
- "[ABG-124] issue", "[ABG-124] issue",
- "test [ABG-124] issue", "test [ABG-124] issue",
- "test [ABG-124 extra] issue", "test [ABG-124 extra] issue",
- "test [ABG-124 issue]", "test [ABG-124 issue]",
- "test [ABG-124]", "test [ABG-124]",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
- Convey("It should render multiple issue mentions in the same line", func() {
- testCases := []string{
- "ABG-124 OTT-4321", "ABG-124 OTT-4321",
- "test ABG-124 OTT-4321", "test ABG-124 OTT-4321",
- "(ABG-124 OTT-4321)", "(ABG-124 OTT-4321)",
- "(ABG-124)(OTT-4321)", "(ABG-124)(OTT-4321)",
- "text ABG-124 test OTT-4321 issue", "text ABG-124 test OTT-4321 issue",
- "A-1 (RRE-345) test", "A-1 (RRE-345) test",
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(string(RenderIssueIndexPattern([]byte(testCases[i]), urlPrefix, metas)), ShouldEqual, testCases[i+1])
- }
- })
+ t.Run("alphanumeric style", func(t *testing.T) {
+ metas := map[string]string{
+ "format": "https://someurl.com/{user}/{repo}/?b={index}",
+ "user": "someuser",
+ "repo": "somerepo",
+ "style": ISSUE_NAME_STYLE_ALPHANUMERIC,
+ }
+
+ tests := []struct {
+ input string
+ expVal string
+ }{
+ {input: "", expVal: ""},
+ {input: "this is a test", expVal: "this is a test"},
+ {input: "test 123 123 1234", expVal: "test 123 123 1234"},
+ {input: "#", expVal: "#"},
+ {input: "##1234", expVal: "##1234"},
+ {input: "# 123", expVal: "# 123"},
+ {input: "#abcd", expVal: "#abcd"},
+ {input: "test #123", expVal: "test #123"},
+ {input: "abc-1234", expVal: "abc-1234"}, // issue prefix must be capital
+ {input: "ABc-1234", expVal: "ABc-1234"}, // issue prefix must be _all_ capital
+ {input: "ABCDEFGHIJK-1234", expVal: "ABCDEFGHIJK-1234"}, // the limit is 10 characters in the prefix
+ {input: "ABC1234", expVal: "ABC1234"}, // dash is required
+ {input: "test ABC- test", expVal: "test ABC- test"}, // number is required
+ {input: "test -1234 test", expVal: "test -1234 test"}, // prefix is required
+ {input: "testABC-123 test", expVal: "testABC-123 test"}, // leading space is required
+ {input: "test ABC-123test", expVal: "test ABC-123test"}, // trailing space is required
+ {input: "ABC-0123", expVal: "ABC-0123"}, // no leading zero
+
+ {input: "OTT-1234 test", expVal: "OTT-1234 test"},
+ {input: "test T-12 issue", expVal: "test T-12 issue"},
+ {input: "test issue ABCDEFGHIJ-1234567890", expVal: "test issue ABCDEFGHIJ-1234567890"},
+ {input: "A-1 test", expVal: "A-1 test"},
+ {input: "test ZED-1 issue", expVal: "test ZED-1 issue"},
+ {input: "test issue DEED-7154", expVal: "test issue DEED-7154"},
+
+ {input: "(ABG-124 issue)", expVal: "(ABG-124 issue)"},
+ {input: "test (ABG-124) issue", expVal: "test (ABG-124) issue"},
+ {input: "test (ABG-124 extra) issue", expVal: "test (ABG-124 extra) issue"},
+ {input: "test (ABG-124 issue)", expVal: "test (ABG-124 issue)"},
+ {input: "test (ABG-124)", expVal: "test (ABG-124)"},
+
+ {input: "[ABG-124] issue", expVal: "[ABG-124] issue"},
+ {input: "test [ABG-124] issue", expVal: "test [ABG-124] issue"},
+ {input: "test [ABG-124 extra] issue", expVal: "test [ABG-124 extra] issue"},
+ {input: "test [ABG-124 issue]", expVal: "test [ABG-124 issue]"},
+ {input: "test [ABG-124]", expVal: "test [ABG-124]"},
+
+ {input: "ABG-124 OTT-4321", expVal: "ABG-124 OTT-4321"},
+ {input: "test ABG-124 OTT-4321", expVal: "test ABG-124 OTT-4321"},
+ {input: "(ABG-124 OTT-4321)", expVal: "(ABG-124 OTT-4321)"},
+ {input: "(ABG-124)(OTT-4321)", expVal: "(ABG-124)(OTT-4321)"},
+ {input: "text ABG-124 test OTT-4321 issue", expVal: "text ABG-124 test OTT-4321 issue"},
+ {input: "A-1 (RRE-345) test", expVal: "A-1 (RRE-345) test"},
+ }
+ for _, test := range tests {
+ t.Run(test.input, func(t *testing.T) {
+ assert.Equal(t, test.expVal, string(RenderIssueIndexPattern([]byte(test.input), urlPrefix, metas)))
+ })
+ }
})
})
}
diff --git a/internal/markup/sanitizer_test.go b/internal/markup/sanitizer_test.go
index 06b108228..4e3672d80 100644
--- a/internal/markup/sanitizer_test.go
+++ b/internal/markup/sanitizer_test.go
@@ -7,32 +7,34 @@ package markup_test
import (
"testing"
- . "github.com/smartystreets/goconvey/convey"
+ "github.com/stretchr/testify/assert"
. "gogs.io/gogs/internal/markup"
)
func Test_Sanitizer(t *testing.T) {
NewSanitizer()
- Convey("Sanitize HTML string and bytes", t, func() {
- testCases := []string{
- // Regular
- `Google`, `Google`,
-
- // Code highlighting class
- ``, ``,
- ``, ``,
- ``, ``,
-
- // Input checkbox
- ``, ``,
- ``, ``,
- ``, ``,
- }
-
- for i := 0; i < len(testCases); i += 2 {
- So(Sanitize(testCases[i]), ShouldEqual, testCases[i+1])
- So(string(SanitizeBytes([]byte(testCases[i]))), ShouldEqual, testCases[i+1])
- }
- })
+ tests := []struct {
+ input string
+ expVal string
+ }{
+ // Regular
+ {input: `Google`, expVal: `Google`},
+
+ // Code highlighting class
+ {input: ``, expVal: ``},
+ {input: ``, expVal: ``},
+ {input: ``, expVal: ``},
+
+ // Input checkbox
+ {input: ``, expVal: ``},
+ {input: ``, expVal: ``},
+ {input: ``, expVal: ``},
+ }
+ for _, test := range tests {
+ t.Run(test.input, func(t *testing.T) {
+ assert.Equal(t, test.expVal, Sanitize(test.input))
+ assert.Equal(t, test.expVal, string(SanitizeBytes([]byte(test.input))))
+ })
+ }
}
diff --git a/internal/mock/locale.go b/internal/mocks/locale.go
similarity index 51%
rename from internal/mock/locale.go
rename to internal/mocks/locale.go
index fbd91da17..ef95e056b 100644
--- a/internal/mock/locale.go
+++ b/internal/mocks/locale.go
@@ -2,7 +2,7 @@
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
-package mock
+package mocks
import (
"gopkg.in/macaron.v1"
@@ -10,24 +10,15 @@ import (
var _ macaron.Locale = (*Locale)(nil)
-// Locale is a mock that implements macaron.Locale.
type Locale struct {
- lang string
- tr func(string, ...interface{}) string
-}
-
-// NewLocale creates a new mock for macaron.Locale.
-func NewLocale(lang string, tr func(string, ...interface{}) string) *Locale {
- return &Locale{
- lang: lang,
- tr: tr,
- }
+ MockLang string
+ MockTr func(string, ...interface{}) string
}
func (l *Locale) Language() string {
- return l.lang
+ return l.MockLang
}
func (l *Locale) Tr(format string, args ...interface{}) string {
- return l.tr(format, args...)
+ return l.MockTr(format, args...)
}
diff --git a/internal/osutil/osutil.go b/internal/osutil/osutil.go
index 3af67570d..b2205a46b 100644
--- a/internal/osutil/osutil.go
+++ b/internal/osutil/osutil.go
@@ -17,6 +17,16 @@ func IsFile(path string) bool {
return !f.IsDir()
}
+// IsDir returns true if given path is a directory, and returns false when it's
+// a file or does not exist.
+func IsDir(dir string) bool {
+ f, e := os.Stat(dir)
+ if e != nil {
+ return false
+ }
+ return f.IsDir()
+}
+
// IsExist returns true if a file or directory exists.
func IsExist(path string) bool {
_, err := os.Stat(path)
diff --git a/internal/osutil/osutil_test.go b/internal/osutil/osutil_test.go
index 8c45f5c0a..ca2c75bf3 100644
--- a/internal/osutil/osutil_test.go
+++ b/internal/osutil/osutil_test.go
@@ -33,6 +33,29 @@ func TestIsFile(t *testing.T) {
}
}
+func TestIsDir(t *testing.T) {
+ tests := []struct {
+ path string
+ expVal bool
+ }{
+ {
+ path: "osutil.go",
+ expVal: false,
+ }, {
+ path: "../osutil",
+ expVal: true,
+ }, {
+ path: "not_found",
+ expVal: false,
+ },
+ }
+ for _, test := range tests {
+ t.Run("", func(t *testing.T) {
+ assert.Equal(t, test.expVal, IsDir(test.path))
+ })
+ }
+}
+
func TestIsExist(t *testing.T) {
tests := []struct {
path string
diff --git a/internal/route/admin/admin.go b/internal/route/admin/admin.go
index 6b0e8c2c1..d1b3bdbfa 100644
--- a/internal/route/admin/admin.go
+++ b/internal/route/admin/admin.go
@@ -22,9 +22,9 @@ import (
)
const (
- DASHBOARD = "admin/dashboard"
- CONFIG = "admin/config"
- MONITOR = "admin/monitor"
+ tmplDashboard = "admin/dashboard"
+ tmplConfig = "admin/config"
+ tmplMonitor = "admin/monitor"
)
// initTime is the time when the application was initialized.
@@ -123,7 +123,7 @@ func Dashboard(c *context.Context) {
// FIXME: update periodically
updateSystemStatus()
c.Data["SysStatus"] = sysStatus
- c.Success(DASHBOARD)
+ c.Success(tmplDashboard)
}
// Operation types.
@@ -209,6 +209,7 @@ func Config(c *context.Context) {
c.Data["Mirror"] = conf.Mirror
c.Data["Webhook"] = conf.Webhook
c.Data["Git"] = conf.Git
+ c.Data["LFS"] = conf.LFS
c.Data["LogRootPath"] = conf.Log.RootPath
type logger struct {
@@ -225,7 +226,7 @@ func Config(c *context.Context) {
}
c.Data["Loggers"] = loggers
- c.Success(CONFIG)
+ c.Success(tmplConfig)
}
func Monitor(c *context.Context) {
@@ -234,5 +235,5 @@ func Monitor(c *context.Context) {
c.Data["PageIsAdminMonitor"] = true
c.Data["Processes"] = process.Processes
c.Data["Entries"] = cron.ListTasks()
- c.Success(MONITOR)
+ c.Success(tmplMonitor)
}
diff --git a/internal/route/admin/auths.go b/internal/route/admin/auths.go
index bcf52e5e4..d2967e293 100644
--- a/internal/route/admin/auths.go
+++ b/internal/route/admin/auths.go
@@ -11,7 +11,6 @@ import (
"github.com/unknwon/com"
log "unknwon.dev/clog/v2"
- "xorm.io/core"
"gogs.io/gogs/internal/auth/ldap"
"gogs.io/gogs/internal/conf"
@@ -32,13 +31,13 @@ func Authentications(c *context.Context) {
c.PageIs("AdminAuthentications")
var err error
- c.Data["Sources"], err = db.LoginSources()
+ c.Data["Sources"], err = db.LoginSources.List(db.ListLoginSourceOpts{})
if err != nil {
c.Error(err, "list login sources")
return
}
- c.Data["Total"] = db.CountLoginSources()
+ c.Data["Total"] = db.LoginSources.Count()
c.Success(AUTHS)
}
@@ -49,16 +48,16 @@ type dropdownItem struct {
var (
authSources = []dropdownItem{
- {db.LoginNames[db.LOGIN_LDAP], db.LOGIN_LDAP},
- {db.LoginNames[db.LOGIN_DLDAP], db.LOGIN_DLDAP},
- {db.LoginNames[db.LOGIN_SMTP], db.LOGIN_SMTP},
- {db.LoginNames[db.LOGIN_PAM], db.LOGIN_PAM},
- {db.LoginNames[db.LOGIN_GITHUB], db.LOGIN_GITHUB},
+ {db.LoginNames[db.LoginLDAP], db.LoginLDAP},
+ {db.LoginNames[db.LoginDLDAP], db.LoginDLDAP},
+ {db.LoginNames[db.LoginSMTP], db.LoginSMTP},
+ {db.LoginNames[db.LoginPAM], db.LoginPAM},
+ {db.LoginNames[db.LoginGitHub], db.LoginGitHub},
}
securityProtocols = []dropdownItem{
- {db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED], ldap.SECURITY_PROTOCOL_UNENCRYPTED},
- {db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_LDAPS], ldap.SECURITY_PROTOCOL_LDAPS},
- {db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_START_TLS], ldap.SECURITY_PROTOCOL_START_TLS},
+ {db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted], ldap.SecurityProtocolUnencrypted},
+ {db.SecurityProtocolNames[ldap.SecurityProtocolLDAPS], ldap.SecurityProtocolLDAPS},
+ {db.SecurityProtocolNames[ldap.SecurityProtocolStartTLS], ldap.SecurityProtocolStartTLS},
}
)
@@ -67,9 +66,9 @@ func NewAuthSource(c *context.Context) {
c.PageIs("Admin")
c.PageIs("AdminAuthentications")
- c.Data["type"] = db.LOGIN_LDAP
- c.Data["CurrentTypeName"] = db.LoginNames[db.LOGIN_LDAP]
- c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SECURITY_PROTOCOL_UNENCRYPTED]
+ c.Data["type"] = db.LoginLDAP
+ c.Data["CurrentTypeName"] = db.LoginNames[db.LoginLDAP]
+ c.Data["CurrentSecurityProtocol"] = db.SecurityProtocolNames[ldap.SecurityProtocolUnencrypted]
c.Data["smtp_auth"] = "PLAIN"
c.Data["is_active"] = true
c.Data["is_default"] = true
@@ -81,7 +80,7 @@ func NewAuthSource(c *context.Context) {
func parseLDAPConfig(f form.Authentication) *db.LDAPConfig {
return &db.LDAPConfig{
- Source: &ldap.Source{
+ Source: ldap.Source{
Host: f.Host,
Port: f.Port,
SecurityProtocol: ldap.SecurityProtocol(f.SecurityProtocol),
@@ -129,19 +128,19 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
c.Data["SMTPAuths"] = db.SMTPAuths
hasTLS := false
- var config core.Conversion
+ var config interface{}
switch db.LoginType(f.Type) {
- case db.LOGIN_LDAP, db.LOGIN_DLDAP:
+ case db.LoginLDAP, db.LoginDLDAP:
config = parseLDAPConfig(f)
- hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SECURITY_PROTOCOL_UNENCRYPTED
- case db.LOGIN_SMTP:
+ hasTLS = ldap.SecurityProtocol(f.SecurityProtocol) > ldap.SecurityProtocolUnencrypted
+ case db.LoginSMTP:
config = parseSMTPConfig(f)
hasTLS = true
- case db.LOGIN_PAM:
+ case db.LoginPAM:
config = &db.PAMConfig{
ServiceName: f.PAMServiceName,
}
- case db.LOGIN_GITHUB:
+ case db.LoginGitHub:
config = &db.GitHubConfig{
APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
}
@@ -156,22 +155,31 @@ func NewAuthSourcePost(c *context.Context, f form.Authentication) {
return
}
- if err := db.CreateLoginSource(&db.LoginSource{
+ source, err := db.LoginSources.Create(db.CreateLoginSourceOpts{
Type: db.LoginType(f.Type),
Name: f.Name,
- IsActived: f.IsActive,
- IsDefault: f.IsDefault,
- Cfg: config,
- }); err != nil {
+ Activated: f.IsActive,
+ Default: f.IsDefault,
+ Config: config,
+ })
+ if err != nil {
if db.IsErrLoginSourceAlreadyExist(err) {
c.FormErr("Name")
- c.RenderWithErr(c.Tr("admin.auths.login_source_exist", err.(db.ErrLoginSourceAlreadyExist).Name), AUTH_NEW, f)
+ c.RenderWithErr(c.Tr("admin.auths.login_source_exist", f.Name), AUTH_NEW, f)
} else {
c.Error(err, "create login source")
}
return
}
+ if source.IsDefault {
+ err = db.LoginSources.ResetNonDefault(source)
+ if err != nil {
+ c.Error(err, "reset non-default login sources")
+ return
+ }
+ }
+
log.Trace("Authentication created by admin(%s): %s", c.User.Name, f.Name)
c.Flash.Success(c.Tr("admin.auths.new_success", f.Name))
@@ -186,7 +194,7 @@ func EditAuthSource(c *context.Context) {
c.Data["SecurityProtocols"] = securityProtocols
c.Data["SMTPAuths"] = db.SMTPAuths
- source, err := db.GetLoginSourceByID(c.ParamsInt64(":authid"))
+ source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
if err != nil {
c.Error(err, "get login source by ID")
return
@@ -204,7 +212,7 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
c.Data["SMTPAuths"] = db.SMTPAuths
- source, err := db.GetLoginSourceByID(c.ParamsInt64(":authid"))
+ source, err := db.LoginSources.GetByID(c.ParamsInt64(":authid"))
if err != nil {
c.Error(err, "get login source by ID")
return
@@ -217,17 +225,17 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
return
}
- var config core.Conversion
+ var config interface{}
switch db.LoginType(f.Type) {
- case db.LOGIN_LDAP, db.LOGIN_DLDAP:
+ case db.LoginLDAP, db.LoginDLDAP:
config = parseLDAPConfig(f)
- case db.LOGIN_SMTP:
+ case db.LoginSMTP:
config = parseSMTPConfig(f)
- case db.LOGIN_PAM:
+ case db.LoginPAM:
config = &db.PAMConfig{
ServiceName: f.PAMServiceName,
}
- case db.LOGIN_GITHUB:
+ case db.LoginGitHub:
config = &db.GitHubConfig{
APIEndpoint: strings.TrimSuffix(f.GitHubAPIEndpoint, "/") + "/",
}
@@ -239,12 +247,20 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
source.Name = f.Name
source.IsActived = f.IsActive
source.IsDefault = f.IsDefault
- source.Cfg = config
- if err := db.UpdateLoginSource(source); err != nil {
+ source.Config = config
+ if err := db.LoginSources.Save(source); err != nil {
c.Error(err, "update login source")
return
}
+ if source.IsDefault {
+ err = db.LoginSources.ResetNonDefault(source)
+ if err != nil {
+ c.Error(err, "reset non-default login sources")
+ return
+ }
+ }
+
log.Trace("Authentication changed by admin '%s': %d", c.User.Name, source.ID)
c.Flash.Success(c.Tr("admin.auths.update_success"))
@@ -252,13 +268,8 @@ func EditAuthSourcePost(c *context.Context, f form.Authentication) {
}
func DeleteAuthSource(c *context.Context) {
- source, err := db.GetLoginSourceByID(c.ParamsInt64(":authid"))
- if err != nil {
- c.Error(err, "get login source by ID")
- return
- }
-
- if err = db.DeleteSource(source); err != nil {
+ id := c.ParamsInt64(":authid")
+ if err := db.LoginSources.DeleteByID(id); err != nil {
if db.IsErrLoginSourceInUse(err) {
c.Flash.Error(c.Tr("admin.auths.still_in_used"))
} else {
@@ -269,7 +280,7 @@ func DeleteAuthSource(c *context.Context) {
})
return
}
- log.Trace("Authentication deleted by admin(%s): %d", c.User.Name, source.ID)
+ log.Trace("Authentication deleted by admin(%s): %d", c.User.Name, id)
c.Flash.Success(c.Tr("admin.auths.deletion_success"))
c.JSONSuccess(map[string]interface{}{
diff --git a/internal/route/admin/orgs.go b/internal/route/admin/orgs.go
index a37624845..e2fd4be55 100644
--- a/internal/route/admin/orgs.go
+++ b/internal/route/admin/orgs.go
@@ -21,7 +21,7 @@ func Organizations(c *context.Context) {
c.Data["PageIsAdminOrganizations"] = true
route.RenderUserSearch(c, &route.UserSearchOptions{
- Type: db.USER_TYPE_ORGANIZATION,
+ Type: db.UserOrganization,
Counter: db.CountOrganizations,
Ranger: db.Organizations,
PageSize: conf.UI.Admin.OrgPagingNum,
diff --git a/internal/route/admin/users.go b/internal/route/admin/users.go
index 630fa4ca0..8a09690d0 100644
--- a/internal/route/admin/users.go
+++ b/internal/route/admin/users.go
@@ -30,9 +30,9 @@ func Users(c *context.Context) {
c.Data["PageIsAdminUsers"] = true
route.RenderUserSearch(c, &route.UserSearchOptions{
- Type: db.USER_TYPE_INDIVIDUAL,
+ Type: db.UserIndividual,
Counter: db.CountUsers,
- Ranger: db.Users,
+ Ranger: db.ListUsers,
PageSize: conf.UI.Admin.UserPagingNum,
OrderBy: "id ASC",
TplName: USERS,
@@ -46,7 +46,7 @@ func NewUser(c *context.Context) {
c.Data["login_type"] = "0-0"
- sources, err := db.LoginSources()
+ sources, err := db.LoginSources.List(db.ListLoginSourceOpts{})
if err != nil {
c.Error(err, "list login sources")
return
@@ -62,7 +62,7 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
c.Data["PageIsAdmin"] = true
c.Data["PageIsAdminUsers"] = true
- sources, err := db.LoginSources()
+ sources, err := db.LoginSources.List(db.ListLoginSourceOpts{})
if err != nil {
c.Error(err, "list login sources")
return
@@ -77,17 +77,15 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
}
u := &db.User{
- Name: f.UserName,
- Email: f.Email,
- Passwd: f.Password,
- IsActive: true,
- LoginType: db.LOGIN_PLAIN,
+ Name: f.UserName,
+ Email: f.Email,
+ Passwd: f.Password,
+ IsActive: true,
}
if len(f.LoginType) > 0 {
fields := strings.Split(f.LoginType, "-")
if len(fields) == 2 {
- u.LoginType = db.LoginType(com.StrTo(fields[0]).MustInt())
u.LoginSource = com.StrTo(fields[1]).MustInt64()
u.LoginName = f.LoginName
}
@@ -101,12 +99,9 @@ func NewUserPost(c *context.Context, f form.AdminCrateUser) {
case db.IsErrEmailAlreadyUsed(err):
c.Data["Err_Email"] = true
c.RenderWithErr(c.Tr("form.email_been_used"), USER_NEW, &f)
- case db.IsErrNameReserved(err):
+ case db.IsErrNameNotAllowed(err):
c.Data["Err_UserName"] = true
- c.RenderWithErr(c.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), USER_NEW, &f)
- case db.IsErrNamePatternNotAllowed(err):
- c.Data["Err_UserName"] = true
- c.RenderWithErr(c.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), USER_NEW, &f)
+ c.RenderWithErr(c.Tr("user.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), USER_NEW, &f)
default:
c.Error(err, "create user")
}
@@ -132,7 +127,7 @@ func prepareUserInfo(c *context.Context) *db.User {
c.Data["User"] = u
if u.LoginSource > 0 {
- c.Data["LoginSource"], err = db.GetLoginSourceByID(u.LoginSource)
+ c.Data["LoginSource"], err = db.LoginSources.GetByID(u.LoginSource)
if err != nil {
c.Error(err, "get login source by ID")
return nil
@@ -141,7 +136,7 @@ func prepareUserInfo(c *context.Context) *db.User {
c.Data["LoginSource"] = &db.LoginSource{}
}
- sources, err := db.LoginSources()
+ sources, err := db.LoginSources.List(db.ListLoginSourceOpts{})
if err != nil {
c.Error(err, "list login sources")
return nil
@@ -183,12 +178,10 @@ func EditUserPost(c *context.Context, f form.AdminEditUser) {
fields := strings.Split(f.LoginType, "-")
if len(fields) == 2 {
- loginType := db.LoginType(com.StrTo(fields[0]).MustInt())
loginSource := com.StrTo(fields[1]).MustInt64()
if u.LoginSource != loginSource {
u.LoginSource = loginSource
- u.LoginType = loginType
}
}
@@ -199,7 +192,7 @@ func EditUserPost(c *context.Context, f form.AdminEditUser) {
c.Error(err, "get user salt")
return
}
- u.EncodePasswd()
+ u.EncodePassword()
}
u.LoginName = f.LoginName
diff --git a/internal/route/api/v1/admin/org_team.go b/internal/route/api/v1/admin/org_team.go
index 953f0936f..b0bc9accf 100644
--- a/internal/route/api/v1/admin/org_team.go
+++ b/internal/route/api/v1/admin/org_team.go
@@ -60,3 +60,17 @@ func RemoveTeamMember(c *context.APIContext) {
c.NoContent()
}
+
+func ListTeamMembers(c *context.APIContext) {
+ team := c.Org.Team
+ if err := team.GetMembers(); err != nil {
+ c.Error(err, "get team members")
+ return
+ }
+
+ apiMembers := make([]*api.User, len(team.Members))
+ for i := range team.Members {
+ apiMembers[i] = team.Members[i].APIFormat()
+ }
+ c.JSONSuccess(apiMembers)
+}
diff --git a/internal/route/api/v1/admin/user.go b/internal/route/api/v1/admin/user.go
index c339edd24..ef0042615 100644
--- a/internal/route/api/v1/admin/user.go
+++ b/internal/route/api/v1/admin/user.go
@@ -13,7 +13,6 @@ import (
"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/email"
"gogs.io/gogs/internal/route/api/v1/user"
)
@@ -23,9 +22,9 @@ func parseLoginSource(c *context.APIContext, u *db.User, sourceID int64, loginNa
return
}
- source, err := db.GetLoginSourceByID(sourceID)
+ source, err := db.LoginSources.GetByID(sourceID)
if err != nil {
- if errors.IsLoginSourceNotExist(err) {
+ if db.IsErrLoginSourceNotExist(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
c.Error(err, "get login source by ID")
@@ -33,19 +32,17 @@ func parseLoginSource(c *context.APIContext, u *db.User, sourceID int64, loginNa
return
}
- u.LoginType = source.Type
u.LoginSource = source.ID
u.LoginName = loginName
}
func CreateUser(c *context.APIContext, form api.CreateUserOption) {
u := &db.User{
- Name: form.Username,
- FullName: form.FullName,
- Email: form.Email,
- Passwd: form.Password,
- IsActive: true,
- LoginType: db.LOGIN_PLAIN,
+ Name: form.Username,
+ FullName: form.FullName,
+ Email: form.Email,
+ Passwd: form.Password,
+ IsActive: true,
}
parseLoginSource(c, u, form.SourceID, form.LoginName)
@@ -56,8 +53,7 @@ func CreateUser(c *context.APIContext, form api.CreateUserOption) {
if err := db.CreateUser(u); err != nil {
if db.IsErrUserAlreadyExist(err) ||
db.IsErrEmailAlreadyUsed(err) ||
- db.IsErrNameReserved(err) ||
- db.IsErrNamePatternNotAllowed(err) {
+ db.IsErrNameNotAllowed(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
c.Error(err, "create user")
@@ -92,7 +88,7 @@ func EditUser(c *context.APIContext, form api.EditUserOption) {
c.Error(err, "get user salt")
return
}
- u.EncodePasswd()
+ u.EncodePassword()
}
u.LoginName = form.LoginName
diff --git a/internal/route/api/v1/api.go b/internal/route/api/v1/api.go
index b56d640b2..993ce8a3a 100644
--- a/internal/route/api/v1/api.go
+++ b/internal/route/api/v1/api.go
@@ -55,7 +55,7 @@ func repoAssignment() macaron.Handler {
}
if c.IsTokenAuth && c.User.IsAdmin {
- c.Repo.AccessMode = db.ACCESS_MODE_OWNER
+ c.Repo.AccessMode = db.AccessModeOwner
} else {
mode, err := db.UserAccessMode(c.UserID(), r)
if err != nil {
@@ -245,6 +245,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/search", repo.Search)
m.Get("/:username/:reponame", repoAssignment(), repo.Get)
+ m.Get("/:username/:reponame/releases", repoAssignment(), repo.Releases)
})
m.Group("/repos", func() {
@@ -395,6 +396,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/teams", func() {
m.Group("/:teamid", func() {
+ m.Get("/members", admin.ListTeamMembers)
m.Combo("/members/:username").
Put(admin.AddTeamMember).
Delete(admin.RemoveTeamMember)
diff --git a/internal/route/api/v1/org/org.go b/internal/route/api/v1/org/org.go
index 0bcefbe4f..7a7de50e1 100644
--- a/internal/route/api/v1/org/org.go
+++ b/internal/route/api/v1/org/org.go
@@ -27,12 +27,11 @@ func CreateOrgForUser(c *context.APIContext, apiForm api.CreateOrgOption, user *
Website: apiForm.Website,
Location: apiForm.Location,
IsActive: true,
- Type: db.USER_TYPE_ORGANIZATION,
+ Type: db.UserOrganization,
}
if err := db.CreateOrganization(org, user); err != nil {
if db.IsErrUserAlreadyExist(err) ||
- db.IsErrNameReserved(err) ||
- db.IsErrNamePatternNotAllowed(err) {
+ db.IsErrNameNotAllowed(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
c.Error(err, "create organization")
diff --git a/internal/route/api/v1/repo/repo.go b/internal/route/api/v1/repo/repo.go
index e198dffcd..138f39e83 100644
--- a/internal/route/api/v1/repo/repo.go
+++ b/internal/route/api/v1/repo/repo.go
@@ -131,8 +131,8 @@ func listUserRepositories(c *context.APIContext, username string) {
i := numOwnRepos
for repo, access := range accessibleRepos {
repos[i] = repo.APIFormat(&api.Permission{
- Admin: access >= db.ACCESS_MODE_ADMIN,
- Push: access >= db.ACCESS_MODE_WRITE,
+ Admin: access >= db.AccessModeAdmin,
+ Push: access >= db.AccessModeWrite,
Pull: true,
})
i++
@@ -165,8 +165,7 @@ func CreateUserRepo(c *context.APIContext, owner *db.User, opt api.CreateRepoOpt
})
if err != nil {
if db.IsErrRepoAlreadyExist(err) ||
- db.IsErrNameReserved(err) ||
- db.IsErrNamePatternNotAllowed(err) {
+ db.IsErrNameNotAllowed(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
if repo != nil {
@@ -403,3 +402,26 @@ func MirrorSync(c *context.APIContext) {
go db.MirrorQueue.Add(repo.ID)
c.Status(http.StatusAccepted)
}
+
+func Releases(c *context.APIContext) {
+ _, repo := parseOwnerAndRepo(c)
+ releases, err := db.GetReleasesByRepoID(repo.ID)
+ if err != nil {
+ c.Error(err, "get releases by repository ID")
+ return
+ }
+ apiReleases := make([]*api.Release, 0, len(releases))
+ for _, r := range releases {
+ publisher, err := db.GetUserByID(r.PublisherID)
+ if err != nil {
+ c.Error(err, "get release publisher")
+ return
+ }
+ r.Publisher = publisher
+ }
+ for _, r := range releases {
+ apiReleases = append(apiReleases, r.APIFormat())
+ }
+
+ c.JSONSuccess(&apiReleases)
+}
diff --git a/internal/route/api/v1/user/app.go b/internal/route/api/v1/user/app.go
index 99a422ccb..5afa34fba 100644
--- a/internal/route/api/v1/user/app.go
+++ b/internal/route/api/v1/user/app.go
@@ -11,11 +11,10 @@ import (
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/db"
- "gogs.io/gogs/internal/db/errors"
)
func ListAccessTokens(c *context.APIContext) {
- tokens, err := db.ListAccessTokens(c.User.ID)
+ tokens, err := db.AccessTokens.List(c.User.ID)
if err != nil {
c.Error(err, "list access tokens")
return
@@ -29,12 +28,9 @@ func ListAccessTokens(c *context.APIContext) {
}
func CreateAccessToken(c *context.APIContext, form api.CreateAccessTokenOption) {
- t := &db.AccessToken{
- UID: c.User.ID,
- Name: form.Name,
- }
- if err := db.NewAccessToken(t); err != nil {
- if errors.IsAccessTokenNameAlreadyExist(err) {
+ t, err := db.AccessTokens.Create(c.User.ID, form.Name)
+ if err != nil {
+ if db.IsErrAccessTokenAlreadyExist(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
c.Error(err, "new access token")
diff --git a/internal/route/api/v1/user/email.go b/internal/route/api/v1/user/email.go
index 07fd4f8af..5584803ec 100644
--- a/internal/route/api/v1/user/email.go
+++ b/internal/route/api/v1/user/email.go
@@ -46,7 +46,7 @@ func AddEmail(c *context.APIContext, form api.CreateEmailOption) {
if err := db.AddEmailAddresses(emails); err != nil {
if db.IsErrEmailAlreadyUsed(err) {
- c.ErrorStatus(http.StatusUnprocessableEntity, errors.New("email address has been used: "+err.(db.ErrEmailAlreadyUsed).Email))
+ c.ErrorStatus(http.StatusUnprocessableEntity, errors.New("email address has been used: "+err.(db.ErrEmailAlreadyUsed).Email()))
} else {
c.Error(err, "add email addresses")
}
diff --git a/internal/route/api/v1/user/user.go b/internal/route/api/v1/user/user.go
index 695d53113..c57f8fed0 100644
--- a/internal/route/api/v1/user/user.go
+++ b/internal/route/api/v1/user/user.go
@@ -19,7 +19,7 @@ import (
func Search(c *context.APIContext) {
opts := &db.SearchUserOptions{
Keyword: c.Query("q"),
- Type: db.USER_TYPE_INDIVIDUAL,
+ Type: db.UserIndividual,
PageSize: com.StrTo(c.Query("limit")).MustInt(),
}
if opts.PageSize == 0 {
diff --git a/internal/route/home.go b/internal/route/home.go
index 3da7f0cfd..d040b957b 100644
--- a/internal/route/home.go
+++ b/internal/route/home.go
@@ -5,7 +5,12 @@
package route
import (
+ "fmt"
+ "net/http"
+
+ "github.com/go-macaron/i18n"
"github.com/unknwon/paginater"
+ "gopkg.in/macaron.v1"
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
@@ -133,9 +138,9 @@ func ExploreUsers(c *context.Context) {
c.Data["PageIsExploreUsers"] = true
RenderUserSearch(c, &UserSearchOptions{
- Type: db.USER_TYPE_INDIVIDUAL,
+ Type: db.UserIndividual,
Counter: db.CountUsers,
- Ranger: db.Users,
+ Ranger: db.ListUsers,
PageSize: conf.UI.ExplorePagingNum,
OrderBy: "updated_unix DESC",
TplName: EXPLORE_USERS,
@@ -148,7 +153,7 @@ func ExploreOrganizations(c *context.Context) {
c.Data["PageIsExploreOrganizations"] = true
RenderUserSearch(c, &UserSearchOptions{
- Type: db.USER_TYPE_ORGANIZATION,
+ Type: db.UserOrganization,
Counter: db.CountOrganizations,
Ranger: db.Organizations,
PageSize: conf.UI.ExplorePagingNum,
@@ -157,7 +162,7 @@ func ExploreOrganizations(c *context.Context) {
})
}
-func NotFound(c *context.Context) {
- c.Data["Title"] = "Page Not Found"
- c.NotFound()
+func NotFound(c *macaron.Context, l i18n.Locale) {
+ c.Data["Title"] = l.Tr("status.page_not_found")
+ c.HTML(http.StatusNotFound, fmt.Sprintf("status/%d", http.StatusNotFound))
}
diff --git a/internal/route/install.go b/internal/route/install.go
index 899393b4e..8c9d2eda4 100644
--- a/internal/route/install.go
+++ b/internal/route/install.go
@@ -27,8 +27,8 @@ import (
"gogs.io/gogs/internal/markup"
"gogs.io/gogs/internal/osutil"
"gogs.io/gogs/internal/ssh"
+ "gogs.io/gogs/internal/strutil"
"gogs.io/gogs/internal/template/highlight"
- "gogs.io/gogs/internal/tool"
)
const (
@@ -76,7 +76,6 @@ func GlobalInit(customConf string) error {
}
db.HasEngine = true
- db.LoadAuthSources()
db.LoadRepoConfig()
db.NewRepoContext()
@@ -86,9 +85,6 @@ func GlobalInit(customConf string) error {
db.InitDeliverHooks()
db.InitTestPullRequests()
}
- if db.EnableSQLite3 {
- log.Info("SQLite3 is supported")
- }
if conf.HasMinWinSvc {
log.Info("Builtin Windows Service is supported")
}
@@ -125,11 +121,7 @@ func InstallInit(c *context.Context) {
c.Title("install.install")
c.PageIs("Install")
- dbOpts := []string{"MySQL", "PostgreSQL", "MSSQL"}
- if db.EnableSQLite3 {
- dbOpts = append(dbOpts, "SQLite3")
- }
- c.Data["DbOptions"] = dbOpts
+ c.Data["DbOptions"] = []string{"MySQL", "PostgreSQL", "MSSQL", "SQLite3"}
}
func Install(c *context.Context) {
@@ -148,9 +140,7 @@ func Install(c *context.Context) {
case "mssql":
c.Data["CurDbOption"] = "MSSQL"
case "sqlite3":
- if db.EnableSQLite3 {
- c.Data["CurDbOption"] = "SQLite3"
- }
+ c.Data["CurDbOption"] = "SQLite3"
}
// Application general settings
@@ -375,7 +365,7 @@ func InstallPost(c *context.Context, f form.Install) {
cfg.Section("log").Key("ROOT_PATH").SetValue(f.LogRootPath)
cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
- secretKey, err := tool.RandomString(15)
+ secretKey, err := strutil.RandomChars(15)
if err != nil {
c.RenderWithErr(c.Tr("install.secret_key_failed", err), INSTALL, &f)
return
diff --git a/internal/route/lfs/basic.go b/internal/route/lfs/basic.go
new file mode 100644
index 000000000..626f5ff08
--- /dev/null
+++ b/internal/route/lfs/basic.go
@@ -0,0 +1,167 @@
+// 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 lfs
+
+import (
+ "encoding/json"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strconv"
+
+ "gopkg.in/macaron.v1"
+ log "unknwon.dev/clog/v2"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/lfsutil"
+ "gogs.io/gogs/internal/strutil"
+)
+
+const transferBasic = "basic"
+const (
+ basicOperationUpload = "upload"
+ basicOperationDownload = "download"
+)
+
+type basicHandler struct {
+ // The default storage backend for uploading new objects.
+ defaultStorage lfsutil.Storage
+ // The list of available storage backends to access objects.
+ storagers map[lfsutil.Storage]lfsutil.Storager
+}
+
+// DefaultStorager returns the default storage backend.
+func (h *basicHandler) DefaultStorager() lfsutil.Storager {
+ return h.storagers[h.defaultStorage]
+}
+
+// Storager returns the given storage backend.
+func (h *basicHandler) Storager(storage lfsutil.Storage) lfsutil.Storager {
+ return h.storagers[storage]
+}
+
+// GET /{owner}/{repo}.git/info/lfs/object/basic/{oid}
+func (h *basicHandler) serveDownload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) {
+ object, err := db.LFS.GetObjectByOID(repo.ID, oid)
+ if err != nil {
+ if db.IsErrLFSObjectNotExist(err) {
+ responseJSON(c.Resp, http.StatusNotFound, responseError{
+ Message: "Object does not exist",
+ })
+ } else {
+ internalServerError(c.Resp)
+ log.Error("Failed to get object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err)
+ }
+ return
+ }
+
+ s := h.Storager(object.Storage)
+ if s == nil {
+ internalServerError(c.Resp)
+ log.Error("Failed to locate the object [repo_id: %d, oid: %s]: storage %q not found", object.RepoID, object.OID, object.Storage)
+ return
+ }
+
+ c.Header().Set("Content-Type", "application/octet-stream")
+ c.Header().Set("Content-Length", strconv.FormatInt(object.Size, 10))
+ c.Status(http.StatusOK)
+
+ err = s.Download(object.OID, c.Resp)
+ if err != nil {
+ log.Error("Failed to download object [oid: %s]: %v", object.OID, err)
+ return
+ }
+}
+
+// PUT /{owner}/{repo}.git/info/lfs/object/basic/{oid}
+func (h *basicHandler) serveUpload(c *macaron.Context, repo *db.Repository, oid lfsutil.OID) {
+ // NOTE: LFS client will retry upload the same object if there was a partial failure,
+ // therefore we would like to skip ones that already exist.
+ _, err := db.LFS.GetObjectByOID(repo.ID, oid)
+ if err == nil {
+ // Object exists, drain the request body and we're good.
+ _, _ = io.Copy(ioutil.Discard, c.Req.Request.Body)
+ c.Req.Request.Body.Close()
+ c.Status(http.StatusOK)
+ return
+ } else if !db.IsErrLFSObjectNotExist(err) {
+ internalServerError(c.Resp)
+ log.Error("Failed to get object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err)
+ return
+ }
+
+ s := h.DefaultStorager()
+ written, err := s.Upload(oid, c.Req.Request.Body)
+ if err != nil {
+ if err == lfsutil.ErrInvalidOID {
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: err.Error(),
+ })
+ } else {
+ internalServerError(c.Resp)
+ log.Error("Failed to upload object [storage: %s, oid: %s]: %v", s.Storage(), oid, err)
+ }
+ return
+ }
+
+ err = db.LFS.CreateObject(repo.ID, oid, written, s.Storage())
+ if err != nil {
+ // NOTE: It is OK to leave the file when the whole operation failed
+ // with a DB error, a retry on client side can safely overwrite the
+ // same file as OID is seen as unique to every file.
+ internalServerError(c.Resp)
+ log.Error("Failed to create object [repo_id: %d, oid: %s]: %v", repo.ID, oid, err)
+ return
+ }
+ c.Status(http.StatusOK)
+
+ log.Trace("[LFS] Object created %q", oid)
+}
+
+// POST /{owner}/{repo}.git/info/lfs/object/basic/verify
+func (h *basicHandler) serveVerify(c *macaron.Context, repo *db.Repository) {
+ var request basicVerifyRequest
+ defer c.Req.Request.Body.Close()
+ err := json.NewDecoder(c.Req.Request.Body).Decode(&request)
+ if err != nil {
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: strutil.ToUpperFirst(err.Error()),
+ })
+ return
+ }
+
+ if !lfsutil.ValidOID(request.Oid) {
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: "Invalid oid",
+ })
+ return
+ }
+
+ object, err := db.LFS.GetObjectByOID(repo.ID, request.Oid)
+ if err != nil {
+ if db.IsErrLFSObjectNotExist(err) {
+ responseJSON(c.Resp, http.StatusNotFound, responseError{
+ Message: "Object does not exist",
+ })
+ } else {
+ internalServerError(c.Resp)
+ log.Error("Failed to get object [repo_id: %d, oid: %s]: %v", repo.ID, request.Oid, err)
+ }
+ return
+ }
+
+ if object.Size != request.Size {
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: "Object size mismatch",
+ })
+ return
+ }
+ c.Status(http.StatusOK)
+}
+
+type basicVerifyRequest struct {
+ Oid lfsutil.OID `json:"oid"`
+ Size int64 `json:"size"`
+}
diff --git a/internal/route/lfs/basic_test.go b/internal/route/lfs/basic_test.go
new file mode 100644
index 000000000..db2fbe616
--- /dev/null
+++ b/internal/route/lfs/basic_test.go
@@ -0,0 +1,288 @@
+// 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 lfs
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/macaron.v1"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/lfsutil"
+)
+
+var _ lfsutil.Storager = (*mockStorage)(nil)
+
+// mockStorage is a in-memory storage for LFS objects.
+type mockStorage struct {
+ buf *bytes.Buffer
+}
+
+func (s *mockStorage) Storage() lfsutil.Storage {
+ return "memory"
+}
+
+func (s *mockStorage) Upload(oid lfsutil.OID, rc io.ReadCloser) (int64, error) {
+ defer rc.Close()
+ return io.Copy(s.buf, rc)
+}
+
+func (s *mockStorage) Download(oid lfsutil.OID, w io.Writer) error {
+ _, err := io.Copy(w, s.buf)
+ return err
+}
+
+func Test_basicHandler_serveDownload(t *testing.T) {
+ s := &mockStorage{}
+ basic := &basicHandler{
+ defaultStorage: s.Storage(),
+ storagers: map[lfsutil.Storage]lfsutil.Storager{
+ s.Storage(): s,
+ },
+ }
+
+ m := macaron.New()
+ m.Use(macaron.Renderer())
+ m.Use(func(c *macaron.Context) {
+ c.Map(&db.Repository{Name: "repo"})
+ c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"))
+ })
+ m.Get("/", basic.serveDownload)
+
+ tests := []struct {
+ name string
+ content string
+ mockLFSStore *db.MockLFSStore
+ expStatusCode int
+ expHeader http.Header
+ expBody string
+ }{
+ {
+ name: "object does not exist",
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return nil, db.ErrLFSObjectNotExist{}
+ },
+ },
+ expStatusCode: http.StatusNotFound,
+ expHeader: http.Header{
+ "Content-Type": []string{"application/vnd.git-lfs+json"},
+ },
+ expBody: `{"message":"Object does not exist"}` + "\n",
+ },
+ {
+ name: "storage not found",
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return &db.LFSObject{Storage: "bad_storage"}, nil
+ },
+ },
+ expStatusCode: http.StatusInternalServerError,
+ expHeader: http.Header{
+ "Content-Type": []string{"application/vnd.git-lfs+json"},
+ },
+ expBody: `{"message":"Internal server error"}` + "\n",
+ },
+
+ {
+ name: "object exists",
+ content: "Hello world!",
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return &db.LFSObject{
+ Size: 12,
+ Storage: s.Storage(),
+ }, nil
+ },
+ },
+ expStatusCode: http.StatusOK,
+ expHeader: http.Header{
+ "Content-Type": []string{"application/octet-stream"},
+ "Content-Length": []string{"12"},
+ },
+ expBody: "Hello world!",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ db.SetMockLFSStore(t, test.mockLFSStore)
+
+ s.buf = bytes.NewBufferString(test.content)
+
+ r, err := http.NewRequest("GET", "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+ assert.Equal(t, test.expHeader, resp.Header)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
+
+func Test_basicHandler_serveUpload(t *testing.T) {
+ s := &mockStorage{buf: &bytes.Buffer{}}
+ basic := &basicHandler{
+ defaultStorage: s.Storage(),
+ storagers: map[lfsutil.Storage]lfsutil.Storager{
+ s.Storage(): s,
+ },
+ }
+
+ m := macaron.New()
+ m.Use(macaron.Renderer())
+ m.Use(func(c *macaron.Context) {
+ c.Map(&db.Repository{Name: "repo"})
+ c.Map(lfsutil.OID("ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"))
+ })
+ m.Put("/", basic.serveUpload)
+
+ tests := []struct {
+ name string
+ mockLFSStore *db.MockLFSStore
+ expStatusCode int
+ expBody string
+ }{
+ {
+ name: "object already exists",
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return &db.LFSObject{}, nil
+ },
+ },
+ expStatusCode: http.StatusOK,
+ },
+ {
+ name: "new object",
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return nil, db.ErrLFSObjectNotExist{}
+ },
+ MockCreateObject: func(repoID int64, oid lfsutil.OID, size int64, storage lfsutil.Storage) error {
+ return nil
+ },
+ },
+ expStatusCode: http.StatusOK,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ db.SetMockLFSStore(t, test.mockLFSStore)
+
+ r, err := http.NewRequest("PUT", "/", strings.NewReader("Hello world!"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
+
+func Test_basicHandler_serveVerify(t *testing.T) {
+ m := macaron.New()
+ m.Use(macaron.Renderer())
+ m.Use(func(c *macaron.Context) {
+ c.Map(&db.Repository{Name: "repo"})
+ })
+ m.Post("/", (&basicHandler{}).serveVerify)
+
+ tests := []struct {
+ name string
+ body string
+ mockLFSStore *db.MockLFSStore
+ expStatusCode int
+ expBody string
+ }{
+ {
+ name: "invalid oid",
+ body: `{"oid": "bad_oid"}`,
+ expStatusCode: http.StatusBadRequest,
+ expBody: `{"message":"Invalid oid"}` + "\n",
+ },
+ {
+ name: "object does not exist",
+ body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`,
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return nil, db.ErrLFSObjectNotExist{}
+ },
+ },
+ expStatusCode: http.StatusNotFound,
+ expBody: `{"message":"Object does not exist"}` + "\n",
+ },
+ {
+ name: "object size mismatch",
+ body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"}`,
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return &db.LFSObject{Size: 12}, nil
+ },
+ },
+ expStatusCode: http.StatusBadRequest,
+ expBody: `{"message":"Object size mismatch"}` + "\n",
+ },
+
+ {
+ name: "object exists",
+ body: `{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size":12}`,
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectByOID: func(repoID int64, oid lfsutil.OID) (*db.LFSObject, error) {
+ return &db.LFSObject{Size: 12}, nil
+ },
+ },
+ expStatusCode: http.StatusOK,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ db.SetMockLFSStore(t, test.mockLFSStore)
+
+ r, err := http.NewRequest("POST", "/", strings.NewReader(test.body))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
diff --git a/internal/route/lfs/batch.go b/internal/route/lfs/batch.go
new file mode 100644
index 000000000..4f720948e
--- /dev/null
+++ b/internal/route/lfs/batch.go
@@ -0,0 +1,176 @@
+// 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 lfs
+
+import (
+ "fmt"
+ "net/http"
+
+ jsoniter "github.com/json-iterator/go"
+ "gopkg.in/macaron.v1"
+ log "unknwon.dev/clog/v2"
+
+ "gogs.io/gogs/internal/conf"
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/lfsutil"
+ "gogs.io/gogs/internal/strutil"
+)
+
+// POST /{owner}/{repo}.git/info/lfs/object/batch
+func serveBatch(c *macaron.Context, owner *db.User, repo *db.Repository) {
+ var request batchRequest
+ defer c.Req.Request.Body.Close()
+ err := jsoniter.NewDecoder(c.Req.Request.Body).Decode(&request)
+ if err != nil {
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: strutil.ToUpperFirst(err.Error()),
+ })
+ return
+ }
+
+ // NOTE: We only support basic transfer as of now.
+ transfer := transferBasic
+ // Example: https://try.gogs.io/gogs/gogs.git/info/lfs/object/basic
+ baseHref := fmt.Sprintf("%s%s/%s.git/info/lfs/objects/basic", conf.Server.ExternalURL, owner.Name, repo.Name)
+
+ objects := make([]batchObject, 0, len(request.Objects))
+ switch request.Operation {
+ case basicOperationUpload:
+ for _, obj := range request.Objects {
+ var actions batchActions
+ if lfsutil.ValidOID(obj.Oid) {
+ actions = batchActions{
+ Upload: &batchAction{
+ Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
+ },
+ Verify: &batchAction{
+ Href: fmt.Sprintf("%s/verify", baseHref),
+ },
+ }
+ } else {
+ actions = batchActions{
+ Error: &batchError{
+ Code: http.StatusUnprocessableEntity,
+ Message: "Object has invalid oid",
+ },
+ }
+ }
+
+ objects = append(objects, batchObject{
+ Oid: obj.Oid,
+ Size: obj.Size,
+ Actions: actions,
+ })
+ }
+
+ case basicOperationDownload:
+ oids := make([]lfsutil.OID, 0, len(request.Objects))
+ for _, obj := range request.Objects {
+ oids = append(oids, obj.Oid)
+ }
+ stored, err := db.LFS.GetObjectsByOIDs(repo.ID, oids...)
+ if err != nil {
+ internalServerError(c.Resp)
+ log.Error("Failed to get objects [repo_id: %d, oids: %v]: %v", repo.ID, oids, err)
+ return
+ }
+ storedSet := make(map[lfsutil.OID]*db.LFSObject, len(stored))
+ for _, obj := range stored {
+ storedSet[obj.OID] = obj
+ }
+
+ for _, obj := range request.Objects {
+ var actions batchActions
+ if stored := storedSet[obj.Oid]; stored != nil {
+ if stored.Size != obj.Size {
+ actions.Error = &batchError{
+ Code: http.StatusUnprocessableEntity,
+ Message: "Object size mismatch",
+ }
+ } else {
+ actions.Download = &batchAction{
+ Href: fmt.Sprintf("%s/%s", baseHref, obj.Oid),
+ }
+ }
+ } else {
+ actions.Error = &batchError{
+ Code: http.StatusNotFound,
+ Message: "Object does not exist",
+ }
+ }
+
+ objects = append(objects, batchObject{
+ Oid: obj.Oid,
+ Size: obj.Size,
+ Actions: actions,
+ })
+ }
+
+ default:
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: "Operation not recognized",
+ })
+ return
+ }
+
+ responseJSON(c.Resp, http.StatusOK, batchResponse{
+ Transfer: transfer,
+ Objects: objects,
+ })
+}
+
+// batchRequest defines the request payload for the batch endpoint.
+type batchRequest struct {
+ Operation string `json:"operation"`
+ Objects []struct {
+ Oid lfsutil.OID `json:"oid"`
+ Size int64 `json:"size"`
+ } `json:"objects"`
+}
+
+type batchError struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+}
+
+type batchAction struct {
+ Href string `json:"href"`
+}
+
+type batchActions struct {
+ Download *batchAction `json:"download,omitempty"`
+ Upload *batchAction `json:"upload,omitempty"`
+ Verify *batchAction `json:"verify,omitempty"`
+ Error *batchError `json:"error,omitempty"`
+}
+
+type batchObject struct {
+ Oid lfsutil.OID `json:"oid"`
+ Size int64 `json:"size"`
+ Actions batchActions `json:"actions"`
+}
+
+// batchResponse defines the response payload for the batch endpoint.
+type batchResponse struct {
+ Transfer string `json:"transfer"`
+ Objects []batchObject `json:"objects"`
+}
+
+type responseError struct {
+ Message string `json:"message"`
+}
+
+const contentType = "application/vnd.git-lfs+json"
+
+func responseJSON(w http.ResponseWriter, status int, v interface{}) {
+ w.Header().Set("Content-Type", contentType)
+ w.WriteHeader(status)
+
+ err := jsoniter.NewEncoder(w).Encode(v)
+ if err != nil {
+ log.Error("Failed to encode JSON: %v", err)
+ return
+ }
+}
diff --git a/internal/route/lfs/batch_test.go b/internal/route/lfs/batch_test.go
new file mode 100644
index 000000000..61cfb562c
--- /dev/null
+++ b/internal/route/lfs/batch_test.go
@@ -0,0 +1,106 @@
+// 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 lfs
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/macaron.v1"
+
+ "gogs.io/gogs/internal/conf"
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/lfsutil"
+)
+
+func Test_serveBatch(t *testing.T) {
+ conf.SetMockServer(t, conf.ServerOpts{
+ ExternalURL: "https://gogs.example.com/",
+ })
+
+ m := macaron.New()
+ m.Use(func(c *macaron.Context) {
+ c.Map(&db.User{Name: "owner"})
+ c.Map(&db.Repository{Name: "repo"})
+ })
+ m.Post("/", serveBatch)
+
+ tests := []struct {
+ name string
+ body string
+ mockLFSStore *db.MockLFSStore
+ expStatusCode int
+ expBody string
+ }{
+ {
+ name: "unrecognized operation",
+ body: `{"operation": "update"}`,
+ expStatusCode: http.StatusBadRequest,
+ expBody: `{"message":"Operation not recognized"}` + "\n",
+ },
+ {
+ name: "upload: contains invalid oid",
+ body: `{
+"operation": "upload",
+"objects": [
+ {"oid": "bad_oid", "size": 123},
+ {"oid": "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size": 123}
+]}`,
+ expStatusCode: http.StatusOK,
+ expBody: `{"transfer":"basic","objects":[{"oid":"bad_oid","size":123,"actions":{"error":{"code":422,"message":"Object has invalid oid"}}},{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f","size":123,"actions":{"upload":{"href":"https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f"},"verify":{"href":"https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/verify"}}}]}` + "\n",
+ },
+ {
+ name: "download: contains non-existent oid and mismatched size",
+ body: `{
+"operation": "download",
+"objects": [
+ {"oid": "bad_oid", "size": 123},
+ {"oid": "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f", "size": 123},
+ {"oid": "5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57", "size": 456}
+]}`,
+ mockLFSStore: &db.MockLFSStore{
+ MockGetObjectsByOIDs: func(repoID int64, oids ...lfsutil.OID) ([]*db.LFSObject, error) {
+ return []*db.LFSObject{
+ {
+ OID: "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+ Size: 1234,
+ }, {
+ OID: "5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57",
+ Size: 456,
+ },
+ }, nil
+ },
+ },
+ expStatusCode: http.StatusOK,
+ expBody: `{"transfer":"basic","objects":[{"oid":"bad_oid","size":123,"actions":{"error":{"code":404,"message":"Object does not exist"}}},{"oid":"ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f","size":123,"actions":{"error":{"code":422,"message":"Object size mismatch"}}},{"oid":"5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57","size":456,"actions":{"download":{"href":"https://gogs.example.com/owner/repo.git/info/lfs/objects/basic/5cac0a318669fadfee734fb340a5f5b70b428ac57a9f4b109cb6e150b2ba7e57"}}}]}` + "\n",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ db.SetMockLFSStore(t, test.mockLFSStore)
+
+ r, err := http.NewRequest("POST", "/", bytes.NewBufferString(test.body))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
diff --git a/internal/route/lfs/main_test.go b/internal/route/lfs/main_test.go
new file mode 100644
index 000000000..ddc04c1b4
--- /dev/null
+++ b/internal/route/lfs/main_test.go
@@ -0,0 +1,30 @@
+// 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 lfs
+
+import (
+ "flag"
+ "fmt"
+ "os"
+ "testing"
+
+ log "unknwon.dev/clog/v2"
+
+ "gogs.io/gogs/internal/testutil"
+)
+
+func TestMain(m *testing.M) {
+ flag.Parse()
+ if !testing.Verbose() {
+ // Remove the primary logger and register a noop logger.
+ log.Remove(log.DefaultConsoleName)
+ err := log.New("noop", testutil.InitNoopLogger)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ }
+ os.Exit(m.Run())
+}
diff --git a/internal/route/lfs/route.go b/internal/route/lfs/route.go
new file mode 100644
index 000000000..a5c253031
--- /dev/null
+++ b/internal/route/lfs/route.go
@@ -0,0 +1,173 @@
+// 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 lfs
+
+import (
+ "net/http"
+ "strings"
+
+ "gopkg.in/macaron.v1"
+ log "unknwon.dev/clog/v2"
+
+ "gogs.io/gogs/internal/authutil"
+ "gogs.io/gogs/internal/conf"
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/lfsutil"
+)
+
+// RegisterRoutes registers LFS routes using given router, and inherits all groups and middleware.
+func RegisterRoutes(r *macaron.Router) {
+ verifyAccept := verifyHeader("Accept", contentType, http.StatusNotAcceptable)
+ verifyContentTypeJSON := verifyHeader("Content-Type", contentType, http.StatusBadRequest)
+ verifyContentTypeStream := verifyHeader("Content-Type", "application/octet-stream", http.StatusBadRequest)
+
+ r.Group("", func() {
+ r.Post("/objects/batch", authorize(db.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch)
+ r.Group("/objects/basic", func() {
+
+ basic := &basicHandler{
+ defaultStorage: lfsutil.Storage(conf.LFS.Storage),
+ storagers: map[lfsutil.Storage]lfsutil.Storager{
+ lfsutil.StorageLocal: &lfsutil.LocalStorage{Root: conf.LFS.ObjectsPath},
+ },
+ }
+ r.Combo("/:oid", verifyOID()).
+ Get(authorize(db.AccessModeRead), basic.serveDownload).
+ Put(authorize(db.AccessModeWrite), verifyContentTypeStream, basic.serveUpload)
+ r.Post("/verify", authorize(db.AccessModeWrite), verifyAccept, verifyContentTypeJSON, basic.serveVerify)
+ })
+ }, authenticate())
+}
+
+// authenticate tries to authenticate user via HTTP Basic Auth. It first tries to authenticate
+// as plain username and password, then use username as access token if previous step failed.
+func authenticate() macaron.Handler {
+ askCredentials := func(w http.ResponseWriter) {
+ w.Header().Set("Lfs-Authenticate", `Basic realm="Git LFS"`)
+ responseJSON(w, http.StatusUnauthorized, responseError{
+ Message: "Credentials needed",
+ })
+ }
+
+ return func(c *macaron.Context) {
+ username, password := authutil.DecodeBasic(c.Req.Header)
+ if username == "" {
+ askCredentials(c.Resp)
+ return
+ }
+
+ user, err := db.Users.Authenticate(username, password, -1)
+ if err != nil && !db.IsErrUserNotExist(err) {
+ internalServerError(c.Resp)
+ log.Error("Failed to authenticate user [name: %s]: %v", username, err)
+ return
+ }
+
+ if err == nil && user.IsEnabledTwoFactor() {
+ c.Error(http.StatusBadRequest, "Users with 2FA enabled are not allowed to authenticate via username and password.")
+ return
+ }
+
+ // If username and password authentication failed, try again using username as an access token.
+ if db.IsErrUserNotExist(err) {
+ token, err := db.AccessTokens.GetBySHA(username)
+ if err != nil {
+ if db.IsErrAccessTokenNotExist(err) {
+ askCredentials(c.Resp)
+ } else {
+ internalServerError(c.Resp)
+ log.Error("Failed to get access token [sha: %s]: %v", username, err)
+ }
+ return
+ }
+ if err = db.AccessTokens.Save(token); err != nil {
+ log.Error("Failed to update access token: %v", err)
+ }
+
+ user, err = db.Users.GetByID(token.UserID)
+ if err != nil {
+ // Once we found the token, we're supposed to find its related user,
+ // thus any error is unexpected.
+ internalServerError(c.Resp)
+ log.Error("Failed to get user [id: %d]: %v", token.UserID, err)
+ return
+ }
+ }
+
+ log.Trace("[LFS] Authenticated user: %s", user.Name)
+
+ c.Map(user)
+ }
+}
+
+// authorize tries to authorize the user to the context repository with given access mode.
+func authorize(mode db.AccessMode) macaron.Handler {
+ return func(c *macaron.Context, actor *db.User) {
+ username := c.Params(":username")
+ reponame := strings.TrimSuffix(c.Params(":reponame"), ".git")
+
+ owner, err := db.Users.GetByUsername(username)
+ if err != nil {
+ if db.IsErrUserNotExist(err) {
+ c.Status(http.StatusNotFound)
+ } else {
+ internalServerError(c.Resp)
+ log.Error("Failed to get user [name: %s]: %v", username, err)
+ }
+ return
+ }
+
+ repo, err := db.Repos.GetByName(owner.ID, reponame)
+ if err != nil {
+ if db.IsErrRepoNotExist(err) {
+ c.Status(http.StatusNotFound)
+ } else {
+ internalServerError(c.Resp)
+ log.Error("Failed to get repository [owner_id: %d, name: %s]: %v", owner.ID, reponame, err)
+ }
+ return
+ }
+
+ if !db.Perms.Authorize(actor.ID, repo, mode) {
+ c.Status(http.StatusNotFound)
+ return
+ }
+
+ c.Map(owner) // NOTE: Override actor
+ c.Map(repo)
+ }
+}
+
+// verifyHeader checks if the HTTP header contains given value.
+// When not, response given "failCode" as status code.
+func verifyHeader(key, value string, failCode int) macaron.Handler {
+ return func(c *macaron.Context) {
+ if !strings.Contains(c.Req.Header.Get(key), value) {
+ c.Status(failCode)
+ return
+ }
+ }
+}
+
+// verifyOID checks if the ":oid" URL parameter is valid.
+func verifyOID() macaron.Handler {
+ return func(c *macaron.Context) {
+ oid := lfsutil.OID(c.Params(":oid"))
+ if !lfsutil.ValidOID(oid) {
+ responseJSON(c.Resp, http.StatusBadRequest, responseError{
+ Message: "Invalid oid",
+ })
+ return
+ }
+
+ c.Map(oid)
+ }
+}
+
+func internalServerError(w http.ResponseWriter) {
+ responseJSON(w, http.StatusInternalServerError, responseError{
+ Message: "Internal server error",
+ })
+}
diff --git a/internal/route/lfs/route_test.go b/internal/route/lfs/route_test.go
new file mode 100644
index 000000000..d2a95e36e
--- /dev/null
+++ b/internal/route/lfs/route_test.go
@@ -0,0 +1,378 @@
+// 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 lfs
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "gopkg.in/macaron.v1"
+
+ "gogs.io/gogs/internal/db"
+ "gogs.io/gogs/internal/lfsutil"
+)
+
+func Test_authenticate(t *testing.T) {
+ m := macaron.New()
+ m.Use(macaron.Renderer())
+ m.Get("/", authenticate(), func(w http.ResponseWriter, user *db.User) {
+ fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
+ })
+
+ tests := []struct {
+ name string
+ header http.Header
+ mockUsersStore *db.MockUsersStore
+ mockTwoFactorsStore *db.MockTwoFactorsStore
+ mockAccessTokensStore *db.MockAccessTokensStore
+ expStatusCode int
+ expHeader http.Header
+ expBody string
+ }{
+ {
+ name: "no authorization",
+ expStatusCode: http.StatusUnauthorized,
+ expHeader: http.Header{
+ "Lfs-Authenticate": []string{`Basic realm="Git LFS"`},
+ "Content-Type": []string{"application/vnd.git-lfs+json"},
+ },
+ expBody: `{"message":"Credentials needed"}` + "\n",
+ },
+ {
+ name: "user has 2FA enabled",
+ header: http.Header{
+ "Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
+ },
+ mockUsersStore: &db.MockUsersStore{
+ MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
+ return &db.User{}, nil
+ },
+ },
+ mockTwoFactorsStore: &db.MockTwoFactorsStore{
+ MockIsUserEnabled: func(userID int64) bool {
+ return true
+ },
+ },
+ expStatusCode: http.StatusBadRequest,
+ expHeader: http.Header{},
+ expBody: "Users with 2FA enabled are not allowed to authenticate via username and password.",
+ },
+ {
+ name: "both user and access token do not exist",
+ header: http.Header{
+ "Authorization": []string{"Basic dXNlcm5hbWU="},
+ },
+ mockUsersStore: &db.MockUsersStore{
+ MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
+ return nil, db.ErrUserNotExist{}
+ },
+ },
+ mockAccessTokensStore: &db.MockAccessTokensStore{
+ MockGetBySHA: func(sha string) (*db.AccessToken, error) {
+ return nil, db.ErrAccessTokenNotExist{}
+ },
+ },
+ expStatusCode: http.StatusUnauthorized,
+ expHeader: http.Header{
+ "Lfs-Authenticate": []string{`Basic realm="Git LFS"`},
+ "Content-Type": []string{"application/vnd.git-lfs+json"},
+ },
+ expBody: `{"message":"Credentials needed"}` + "\n",
+ },
+
+ {
+ name: "authenticated by username and password",
+ header: http.Header{
+ "Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
+ },
+ mockUsersStore: &db.MockUsersStore{
+ MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
+ return &db.User{ID: 1, Name: "unknwon"}, nil
+ },
+ },
+ mockTwoFactorsStore: &db.MockTwoFactorsStore{
+ MockIsUserEnabled: func(userID int64) bool {
+ return false
+ },
+ },
+ expStatusCode: http.StatusOK,
+ expHeader: http.Header{},
+ expBody: "ID: 1, Name: unknwon",
+ },
+ {
+ name: "authenticate by access token",
+ header: http.Header{
+ "Authorization": []string{"Basic dXNlcm5hbWU="},
+ },
+ mockUsersStore: &db.MockUsersStore{
+ MockAuthenticate: func(username, password string, loginSourceID int64) (*db.User, error) {
+ return nil, db.ErrUserNotExist{}
+ },
+ MockGetByID: func(id int64) (*db.User, error) {
+ return &db.User{ID: 1, Name: "unknwon"}, nil
+ },
+ },
+ mockAccessTokensStore: &db.MockAccessTokensStore{
+ MockGetBySHA: func(sha string) (*db.AccessToken, error) {
+ return &db.AccessToken{}, nil
+ },
+ MockSave: func(t *db.AccessToken) error {
+ return nil
+ },
+ },
+ expStatusCode: http.StatusOK,
+ expHeader: http.Header{},
+ expBody: "ID: 1, Name: unknwon",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ db.SetMockUsersStore(t, test.mockUsersStore)
+ db.SetMockTwoFactorsStore(t, test.mockTwoFactorsStore)
+ db.SetMockAccessTokensStore(t, test.mockAccessTokensStore)
+
+ r, err := http.NewRequest("GET", "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Header = test.header
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+ assert.Equal(t, test.expHeader, resp.Header)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
+
+func Test_authorize(t *testing.T) {
+ tests := []struct {
+ name string
+ authroize macaron.Handler
+ mockUsersStore *db.MockUsersStore
+ mockReposStore *db.MockReposStore
+ mockPermsStore *db.MockPermsStore
+ expStatusCode int
+ expBody string
+ }{
+ {
+ name: "user does not exist",
+ authroize: authorize(db.AccessModeNone),
+ mockUsersStore: &db.MockUsersStore{
+ MockGetByUsername: func(username string) (*db.User, error) {
+ return nil, db.ErrUserNotExist{}
+ },
+ },
+ expStatusCode: http.StatusNotFound,
+ },
+ {
+ name: "repository does not exist",
+ authroize: authorize(db.AccessModeNone),
+ mockUsersStore: &db.MockUsersStore{
+ MockGetByUsername: func(username string) (*db.User, error) {
+ return &db.User{Name: username}, nil
+ },
+ },
+ mockReposStore: &db.MockReposStore{
+ MockGetByName: func(ownerID int64, name string) (*db.Repository, error) {
+ return nil, db.ErrRepoNotExist{}
+ },
+ },
+ expStatusCode: http.StatusNotFound,
+ },
+ {
+ name: "actor is not authorized",
+ authroize: authorize(db.AccessModeWrite),
+ mockUsersStore: &db.MockUsersStore{
+ MockGetByUsername: func(username string) (*db.User, error) {
+ return &db.User{Name: username}, nil
+ },
+ },
+ mockReposStore: &db.MockReposStore{
+ MockGetByName: func(ownerID int64, name string) (*db.Repository, error) {
+ return &db.Repository{Name: name}, nil
+ },
+ },
+ mockPermsStore: &db.MockPermsStore{
+ MockAuthorize: func(userID int64, repo *db.Repository, desired db.AccessMode) bool {
+ return desired <= db.AccessModeRead
+ },
+ },
+ expStatusCode: http.StatusNotFound,
+ },
+
+ {
+ name: "actor is authorized",
+ authroize: authorize(db.AccessModeRead),
+ mockUsersStore: &db.MockUsersStore{
+ MockGetByUsername: func(username string) (*db.User, error) {
+ return &db.User{Name: username}, nil
+ },
+ },
+ mockReposStore: &db.MockReposStore{
+ MockGetByName: func(ownerID int64, name string) (*db.Repository, error) {
+ return &db.Repository{Name: name}, nil
+ },
+ },
+ mockPermsStore: &db.MockPermsStore{
+ MockAuthorize: func(userID int64, repo *db.Repository, desired db.AccessMode) bool {
+ return desired <= db.AccessModeRead
+ },
+ },
+ expStatusCode: http.StatusOK,
+ expBody: "owner.Name: owner, repo.Name: repo",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ db.SetMockUsersStore(t, test.mockUsersStore)
+ db.SetMockReposStore(t, test.mockReposStore)
+ db.SetMockPermsStore(t, test.mockPermsStore)
+
+ m := macaron.New()
+ m.Use(macaron.Renderer())
+ m.Use(func(c *macaron.Context) {
+ c.Map(&db.User{})
+ })
+ m.Get("/:username/:reponame", test.authroize, func(w http.ResponseWriter, owner *db.User, repo *db.Repository) {
+ fmt.Fprintf(w, "owner.Name: %s, repo.Name: %s", owner.Name, repo.Name)
+ })
+
+ r, err := http.NewRequest("GET", "/owner/repo", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
+
+func Test_verifyHeader(t *testing.T) {
+ tests := []struct {
+ name string
+ verifyHeader macaron.Handler
+ header http.Header
+ expStatusCode int
+ }{
+ {
+ name: "header not found",
+ verifyHeader: verifyHeader("Accept", contentType, http.StatusNotAcceptable),
+ expStatusCode: http.StatusNotAcceptable,
+ },
+
+ {
+ name: "header found",
+ verifyHeader: verifyHeader("Accept", "application/vnd.git-lfs+json", http.StatusNotAcceptable),
+ header: http.Header{
+ "Accept": []string{"application/vnd.git-lfs+json; charset=utf-8"},
+ },
+ expStatusCode: http.StatusOK,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ m := macaron.New()
+ m.Use(macaron.Renderer())
+ m.Get("/", test.verifyHeader)
+
+ r, err := http.NewRequest("GET", "/", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ r.Header = test.header
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+ })
+ }
+}
+
+func Test_verifyOID(t *testing.T) {
+ m := macaron.New()
+ m.Get("/:oid", verifyOID(), func(w http.ResponseWriter, oid lfsutil.OID) {
+ fmt.Fprintf(w, "oid: %s", oid)
+ })
+
+ tests := []struct {
+ name string
+ url string
+ expStatusCode int
+ expBody string
+ }{
+ {
+ name: "bad oid",
+ url: "/bad_oid",
+ expStatusCode: http.StatusBadRequest,
+ expBody: `{"message":"Invalid oid"}` + "\n",
+ },
+
+ {
+ name: "good oid",
+ url: "/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+ expStatusCode: http.StatusOK,
+ expBody: "oid: ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ r, err := http.NewRequest("GET", test.url, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ m.ServeHTTP(rr, r)
+
+ resp := rr.Result()
+ assert.Equal(t, test.expStatusCode, resp.StatusCode)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, test.expBody, string(body))
+ })
+ }
+}
+
+func Test_internalServerError(t *testing.T) {
+ rr := httptest.NewRecorder()
+ internalServerError(rr)
+
+ resp := rr.Result()
+ assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, `{"message":"Internal server error"}`+"\n", string(body))
+}
diff --git a/internal/route/org/org.go b/internal/route/org/org.go
index e0c358acd..424cbd153 100644
--- a/internal/route/org/org.go
+++ b/internal/route/org/org.go
@@ -32,7 +32,7 @@ func CreatePost(c *context.Context, f form.CreateOrg) {
org := &db.User{
Name: f.OrgName,
IsActive: true,
- Type: db.USER_TYPE_ORGANIZATION,
+ Type: db.UserOrganization,
}
if err := db.CreateOrganization(org, c.User); err != nil {
@@ -40,10 +40,8 @@ func CreatePost(c *context.Context, f form.CreateOrg) {
switch {
case db.IsErrUserAlreadyExist(err):
c.RenderWithErr(c.Tr("form.org_name_been_taken"), CREATE, &f)
- case db.IsErrNameReserved(err):
- c.RenderWithErr(c.Tr("org.form.name_reserved", err.(db.ErrNameReserved).Name), CREATE, &f)
- case db.IsErrNamePatternNotAllowed(err):
- c.RenderWithErr(c.Tr("org.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), CREATE, &f)
+ case db.IsErrNameNotAllowed(err):
+ c.RenderWithErr(c.Tr("org.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), CREATE, &f)
default:
c.Error(err, "create organization")
}
diff --git a/internal/route/org/setting.go b/internal/route/org/setting.go
index 15b576233..e3b2bf3c5 100644
--- a/internal/route/org/setting.go
+++ b/internal/route/org/setting.go
@@ -51,10 +51,8 @@ func SettingsPost(c *context.Context, f form.UpdateOrgSetting) {
} else if err = db.ChangeUserName(org, f.Name); err != nil {
c.Data["OrgName"] = true
switch {
- case db.IsErrNameReserved(err):
- c.RenderWithErr(c.Tr("user.form.name_reserved"), SETTINGS_OPTIONS, &f)
- case db.IsErrNamePatternNotAllowed(err):
- c.RenderWithErr(c.Tr("user.form.name_pattern_not_allowed"), SETTINGS_OPTIONS, &f)
+ case db.IsErrNameNotAllowed(err):
+ c.RenderWithErr(c.Tr("user.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), SETTINGS_OPTIONS, &f)
default:
c.Error(err, "change user name")
}
@@ -110,7 +108,7 @@ func SettingsDelete(c *context.Context) {
org := c.Org.Organization
if c.Req.Method == "POST" {
- if _, err := db.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
+ if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
if db.IsErrUserNotExist(err) {
c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
} else {
diff --git a/internal/route/org/teams.go b/internal/route/org/teams.go
index a703a82d6..c681ec9fb 100644
--- a/internal/route/org/teams.go
+++ b/internal/route/org/teams.go
@@ -172,8 +172,8 @@ func NewTeamPost(c *context.Context, f form.CreateTeam) {
switch {
case db.IsErrTeamAlreadyExist(err):
c.RenderWithErr(c.Tr("form.team_name_been_taken"), TEAM_NEW, &f)
- case db.IsErrNameReserved(err):
- c.RenderWithErr(c.Tr("org.form.team_name_reserved", err.(db.ErrNameReserved).Name), TEAM_NEW, &f)
+ case db.IsErrNameNotAllowed(err):
+ c.RenderWithErr(c.Tr("org.form.team_name_not_allowed", err.(db.ErrNameNotAllowed).Value()), TEAM_NEW, &f)
default:
c.Error(err, "new team")
}
@@ -228,11 +228,11 @@ func EditTeamPost(c *context.Context, f form.CreateTeam) {
var auth db.AccessMode
switch f.Permission {
case "read":
- auth = db.ACCESS_MODE_READ
+ auth = db.AccessModeRead
case "write":
- auth = db.ACCESS_MODE_WRITE
+ auth = db.AccessModeWrite
case "admin":
- auth = db.ACCESS_MODE_ADMIN
+ auth = db.AccessModeAdmin
default:
c.Status(http.StatusUnauthorized)
return
diff --git a/internal/route/repo/commit.go b/internal/route/repo/commit.go
index e8ff33808..00be5b1a5 100644
--- a/internal/route/repo/commit.go
+++ b/internal/route/repo/commit.go
@@ -152,16 +152,18 @@ func Diff(c *context.Context) {
c.Data["Username"] = userName
c.Data["Reponame"] = repoName
c.Data["IsImageFile"] = commit.IsImageFile
+ c.Data["IsImageFileByIndex"] = commit.IsImageFileByIndex
c.Data["Commit"] = commit
c.Data["Author"] = db.ValidateCommitWithEmail(commit)
c.Data["Diff"] = diff
c.Data["Parents"] = parents
c.Data["DiffNotAvailable"] = diff.NumFiles() == 0
c.Data["SourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", commitID)
+ c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "raw", commitID)
if commit.ParentsCount() > 0 {
c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", parents[0])
+ c.Data["BeforeRawPath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "raw", parents[0])
}
- c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "raw", commitID)
c.Success(DIFF)
}
@@ -213,12 +215,14 @@ func CompareDiff(c *context.Context) {
c.Data["Username"] = userName
c.Data["Reponame"] = repoName
c.Data["IsImageFile"] = commit.IsImageFile
+ c.Data["IsImageFileByIndex"] = commit.IsImageFileByIndex
c.Data["Title"] = "Comparing " + tool.ShortSHA1(beforeCommitID) + "..." + tool.ShortSHA1(afterCommitID) + " · " + userName + "/" + repoName
c.Data["Commit"] = commit
c.Data["Diff"] = diff
c.Data["DiffNotAvailable"] = diff.NumFiles() == 0
c.Data["SourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", afterCommitID)
- c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", beforeCommitID)
c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "raw", afterCommitID)
+ c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "src", beforeCommitID)
+ c.Data["BeforeRawPath"] = conf.Server.Subpath + "/" + path.Join(userName, repoName, "raw", beforeCommitID)
c.Success(DIFF)
}
diff --git a/internal/route/repo/editor.go b/internal/route/repo/editor.go
index cd71ff798..ee0048037 100644
--- a/internal/route/repo/editor.go
+++ b/internal/route/repo/editor.go
@@ -24,10 +24,10 @@ import (
)
const (
- EDIT_FILE = "repo/editor/edit"
- EDIT_DIFF_PREVIEW = "repo/editor/diff_preview"
- DELETE_FILE = "repo/editor/delete"
- UPLOAD_FILE = "repo/editor/upload"
+ tmplEditorEdit = "repo/editor/edit"
+ tmplEditorDiffPreview = "repo/editor/diff_preview"
+ tmplEditorDelete = "repo/editor/delete"
+ tmplEditorUpload = "repo/editor/upload"
)
// getParentTreeFields returns list of parent tree names and corresponding tree paths
@@ -108,7 +108,7 @@ func editFile(c *context.Context, isNewFile bool) {
c.Data["PreviewableFileModes"] = strings.Join(conf.Repository.Editor.PreviewableFileModes, ",")
c.Data["EditorconfigURLPrefix"] = fmt.Sprintf("%s/api/v1/repos/%s/editorconfig/", conf.Server.Subpath, c.Repo.Repository.FullName())
- c.Success(EDIT_FILE)
+ c.Success(tmplEditorEdit)
}
func EditFile(c *context.Context) {
@@ -154,20 +154,20 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
c.Data["PreviewableFileModes"] = strings.Join(conf.Repository.Editor.PreviewableFileModes, ",")
if c.HasError() {
- c.Success(EDIT_FILE)
+ c.Success(tmplEditorEdit)
return
}
if len(f.TreePath) == 0 {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.filename_cannot_be_empty"), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.filename_cannot_be_empty"), tmplEditorEdit, &f)
return
}
if oldBranchName != branchName {
if _, err := c.Repo.Repository.GetBranch(branchName); err == nil {
c.FormErr("NewBranchName")
- c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), tmplEditorEdit, &f)
return
}
}
@@ -188,17 +188,17 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
if index != len(treeNames)-1 {
if !entry.IsTree() {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), tmplEditorEdit, &f)
return
}
} else {
if entry.IsSymlink() {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.file_is_a_symlink", part), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.file_is_a_symlink", part), tmplEditorEdit, &f)
return
} else if entry.IsTree() {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.filename_is_a_directory", part), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.filename_is_a_directory", part), tmplEditorEdit, &f)
return
}
}
@@ -209,7 +209,7 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
if err != nil {
if gitutil.IsErrRevisionNotExist(err) {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), tmplEditorEdit, &f)
} else {
c.Error(err, "get tree entry")
}
@@ -224,7 +224,7 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
for _, file := range files {
if file == f.TreePath {
- c.RenderWithErr(c.Tr("repo.editor.file_changed_while_editing", c.Repo.RepoLink+"/compare/"+lastCommit+"..."+c.Repo.CommitID), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.file_changed_while_editing", c.Repo.RepoLink+"/compare/"+lastCommit+"..."+c.Repo.CommitID), tmplEditorEdit, &f)
return
}
}
@@ -242,7 +242,7 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
}
if entry != nil {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.file_already_exists", f.TreePath), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.file_already_exists", f.TreePath), tmplEditorEdit, &f)
return
}
}
@@ -273,7 +273,7 @@ func editFilePost(c *context.Context, f form.EditRepoFile, isNewFile bool) {
}); err != nil {
log.Error("Failed to update repo file: %v", err)
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.fail_to_update_file", f.TreePath, errors.InternalServerError), EDIT_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.fail_to_update_file", f.TreePath, errors.InternalServerError), tmplEditorEdit, &f)
return
}
@@ -316,7 +316,7 @@ func DiffPreviewPost(c *context.Context, f form.EditPreviewDiff) {
}
c.Data["File"] = diff.Files[0]
- c.Success(EDIT_DIFF_PREVIEW)
+ c.Success(tmplEditorDiffPreview)
}
func DeleteFile(c *context.Context) {
@@ -327,7 +327,7 @@ func DeleteFile(c *context.Context) {
c.Data["commit_message"] = ""
c.Data["commit_choice"] = "direct"
c.Data["new_branch_name"] = ""
- c.Success(DELETE_FILE)
+ c.Success(tmplEditorDelete)
}
func DeleteFilePost(c *context.Context, f form.DeleteRepoFile) {
@@ -349,14 +349,14 @@ func DeleteFilePost(c *context.Context, f form.DeleteRepoFile) {
c.Data["new_branch_name"] = branchName
if c.HasError() {
- c.Success(DELETE_FILE)
+ c.Success(tmplEditorDelete)
return
}
if oldBranchName != branchName {
if _, err := c.Repo.Repository.GetBranch(branchName); err == nil {
c.FormErr("NewBranchName")
- c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), DELETE_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), tmplEditorDelete, &f)
return
}
}
@@ -379,7 +379,7 @@ func DeleteFilePost(c *context.Context, f form.DeleteRepoFile) {
Message: message,
}); err != nil {
log.Error("Failed to delete repo file: %v", err)
- c.RenderWithErr(c.Tr("repo.editor.fail_to_delete_file", c.Repo.TreePath, errors.InternalServerError), DELETE_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.fail_to_delete_file", c.Repo.TreePath, errors.InternalServerError), tmplEditorDelete, &f)
return
}
@@ -415,7 +415,7 @@ func UploadFile(c *context.Context) {
c.Data["commit_message"] = ""
c.Data["commit_choice"] = "direct"
c.Data["new_branch_name"] = ""
- c.Success(UPLOAD_FILE)
+ c.Success(tmplEditorUpload)
}
func UploadFilePost(c *context.Context, f form.UploadRepoFile) {
@@ -446,14 +446,14 @@ func UploadFilePost(c *context.Context, f form.UploadRepoFile) {
c.Data["new_branch_name"] = branchName
if c.HasError() {
- c.Success(UPLOAD_FILE)
+ c.Success(tmplEditorUpload)
return
}
if oldBranchName != branchName {
if _, err := c.Repo.Repository.GetBranch(branchName); err == nil {
c.FormErr("NewBranchName")
- c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), UPLOAD_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.branch_already_exists", branchName), tmplEditorUpload, &f)
return
}
}
@@ -475,7 +475,7 @@ func UploadFilePost(c *context.Context, f form.UploadRepoFile) {
// User can only upload files to a directory.
if !entry.IsTree() {
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), UPLOAD_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.directory_is_a_file", part), tmplEditorUpload, &f)
return
}
}
@@ -500,7 +500,7 @@ func UploadFilePost(c *context.Context, f form.UploadRepoFile) {
}); err != nil {
log.Error("Failed to upload files: %v", err)
c.FormErr("TreePath")
- c.RenderWithErr(c.Tr("repo.editor.unable_to_upload_files", f.TreePath, errors.InternalServerError), UPLOAD_FILE, &f)
+ c.RenderWithErr(c.Tr("repo.editor.unable_to_upload_files", f.TreePath, errors.InternalServerError), tmplEditorUpload, &f)
return
}
diff --git a/internal/route/repo/http.go b/internal/route/repo/http.go
index 413b660f9..93a99aef5 100644
--- a/internal/route/repo/http.go
+++ b/internal/route/repo/http.go
@@ -12,6 +12,7 @@ import (
"os"
"os/exec"
"path"
+ "path/filepath"
"strconv"
"strings"
"time"
@@ -20,14 +21,13 @@ import (
log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/conf"
- "gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lazyregexp"
"gogs.io/gogs/internal/tool"
)
type HTTPContext struct {
- *context.Context
+ *macaron.Context
OwnerName string
OwnerSalt string
RepoID int64
@@ -36,17 +36,17 @@ type HTTPContext struct {
}
// askCredentials responses HTTP header and status which informs client to provide credentials.
-func askCredentials(c *context.Context, status int, text string) {
- c.Resp.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
- c.PlainText(status, text)
+func askCredentials(c *macaron.Context, status int, text string) {
+ c.Header().Set("WWW-Authenticate", "Basic realm=\".\"")
+ c.Error(status, text)
}
func HTTPContexter() macaron.Handler {
- return func(c *context.Context) {
+ return func(c *macaron.Context) {
if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
// Set CORS headers for browser-based git clients
- c.Resp.Header().Set("Access-Control-Allow-Origin", conf.HTTP.AccessControlAllowOrigin)
- c.Resp.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
+ c.Header().Set("Access-Control-Allow-Origin", conf.HTTP.AccessControlAllowOrigin)
+ c.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, User-Agent")
// Handle preflight OPTIONS request
if c.Req.Method == "OPTIONS" {
@@ -63,15 +63,25 @@ func HTTPContexter() macaron.Handler {
strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") ||
c.Req.Method == "GET"
- owner, err := db.GetUserByName(ownerName)
+ owner, err := db.Users.GetByUsername(ownerName)
if err != nil {
- c.NotFoundOrError(err, "get user by name")
+ if db.IsErrUserNotExist(err) {
+ c.Status(http.StatusNotFound)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get user [name: %s]: %v", ownerName, err)
+ }
return
}
- repo, err := db.GetRepositoryByName(owner.ID, repoName)
+ repo, err := db.Repos.GetByName(owner.ID, repoName)
if err != nil {
- c.NotFoundOrError(err, "get repository by name")
+ if db.IsErrRepoNotExist(err) {
+ c.Status(http.StatusNotFound)
+ } else {
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get repository [owner_id: %d, name: %s]: %v", owner.ID, repoName, err)
+ }
return
}
@@ -111,31 +121,35 @@ func HTTPContexter() macaron.Handler {
return
}
- authUser, err := db.UserLogin(authUsername, authPassword, -1)
+ authUser, err := db.Users.Authenticate(authUsername, authPassword, -1)
if err != nil && !db.IsErrUserNotExist(err) {
- c.Error(err, "authenticate user")
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to authenticate user [name: %s]: %v", authUsername, err)
return
}
// If username and password combination failed, try again using username as a token.
if authUser == nil {
- token, err := db.GetAccessTokenBySHA(authUsername)
+ token, err := db.AccessTokens.GetBySHA(authUsername)
if err != nil {
- if db.IsErrAccessTokenEmpty(err) || db.IsErrAccessTokenNotExist(err) {
+ if db.IsErrAccessTokenNotExist(err) {
askCredentials(c, http.StatusUnauthorized, "")
} else {
- c.Error(err, "get access token by SHA")
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get access token [sha: %s]: %v", authUsername, err)
}
return
}
- token.Updated = time.Now()
- // TODO: verify or update token.Updated in database
+ if err = db.AccessTokens.Save(token); err != nil {
+ log.Error("Failed to update access token: %v", err)
+ }
- authUser, err = db.GetUserByID(token.UID)
+ authUser, err = db.Users.GetByID(token.UserID)
if err != nil {
// Once we found token, we're supposed to find its related user,
// thus any error is unexpected.
- c.Error(err, "get user by ID")
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get user [id: %d]: %v", token.UserID, err)
return
}
} else if authUser.IsEnabledTwoFactor() {
@@ -144,23 +158,19 @@ Please create and use personal access token on user settings page`)
return
}
- log.Trace("HTTPGit - Authenticated user: %s", authUser.Name)
+ log.Trace("[Git] Authenticated user: %s", authUser.Name)
- mode := db.ACCESS_MODE_WRITE
+ mode := db.AccessModeWrite
if isPull {
- mode = db.ACCESS_MODE_READ
+ mode = db.AccessModeRead
}
- has, err := db.HasAccess(authUser.ID, repo, mode)
- if err != nil {
- c.Error(err, "check access")
- return
- } else if !has {
+ if !db.Perms.Authorize(authUser.ID, repo, mode) {
askCredentials(c, http.StatusForbidden, "User permission denied")
return
}
if !isPull && repo.IsMirror {
- c.PlainText(http.StatusForbidden, "Mirror repository is read-only")
+ c.Error(http.StatusForbidden, "Mirror repository is read-only")
return
}
@@ -367,7 +377,7 @@ func getGitRepoPath(dir string) (string, error) {
dir += ".git"
}
- filename := path.Join(conf.Repository.Root, dir)
+ filename := filepath.Join(conf.Repository.Root, dir)
if _, err := os.Stat(filename); os.IsNotExist(err) {
return "", err
}
@@ -387,7 +397,7 @@ func HTTP(c *HTTPContext) {
// but we only want to output this message only if user is really trying to access
// Git HTTP endpoints.
if conf.Repository.DisableHTTPGit {
- c.PlainText(http.StatusForbidden, "Interacting with repositories by HTTP protocol is disabled")
+ c.Error(http.StatusForbidden, "Interacting with repositories by HTTP protocol is disabled")
return
}
diff --git a/internal/route/repo/pull.go b/internal/route/repo/pull.go
index 14e9a5c32..e17782752 100644
--- a/internal/route/repo/pull.go
+++ b/internal/route/repo/pull.go
@@ -19,7 +19,6 @@ import (
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/form"
"gogs.io/gogs/internal/gitutil"
- "gogs.io/gogs/internal/tool"
)
const (
@@ -137,10 +136,8 @@ func ForkPost(c *context.Context, f form.CreateRepo) {
c.RenderWithErr(c.Tr("repo.form.reach_limit_of_creation", c.User.RepoCreationNum()), FORK, &f)
case db.IsErrRepoAlreadyExist(err):
c.RenderWithErr(c.Tr("repo.settings.new_owner_has_same_repo"), FORK, &f)
- case db.IsErrNameReserved(err):
- c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), FORK, &f)
- case db.IsErrNamePatternNotAllowed(err):
- c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), FORK, &f)
+ case db.IsErrNameNotAllowed(err):
+ c.RenderWithErr(c.Tr("repo.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), FORK, &f)
default:
c.Error(err, "fork repository")
}
@@ -378,6 +375,7 @@ func ViewPullFiles(c *context.Context) {
c.Data["IsSplitStyle"] = c.Query("style") == "split"
c.Data["IsImageFile"] = commit.IsImageFile
+ c.Data["IsImageFileByIndex"] = commit.IsImageFileByIndex
// It is possible head repo has been deleted for merged pull requests
if pull.HeadRepo != nil {
@@ -386,8 +384,9 @@ func ViewPullFiles(c *context.Context) {
headTarget := path.Join(pull.HeadUserName, pull.HeadRepo.Name)
c.Data["SourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", endCommitID)
- c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", startCommitID)
c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "raw", endCommitID)
+ c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", startCommitID)
+ c.Data["BeforeRawPath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "raw", startCommitID)
}
c.Data["RequireHighlightJS"] = true
@@ -595,11 +594,13 @@ func PrepareCompareDiff(
c.Data["Username"] = headUser.Name
c.Data["Reponame"] = headRepo.Name
c.Data["IsImageFile"] = headCommit.IsImageFile
+ c.Data["IsImageFileByIndex"] = headCommit.IsImageFileByIndex
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", meta.MergeBase)
c.Data["RawPath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "raw", headCommitID)
+ c.Data["BeforeSourcePath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "src", meta.MergeBase)
+ c.Data["BeforeRawPath"] = conf.Server.Subpath + "/" + path.Join(headTarget, "raw", meta.MergeBase)
return false
}
@@ -740,51 +741,3 @@ func CompareAndPullRequestPost(c *context.Context, f form.NewIssue) {
log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
c.Redirect(c.Repo.RepoLink + "/pulls/" + com.ToStr(pullIssue.Index))
}
-
-func parseOwnerAndRepo(c *context.Context) (*db.User, *db.Repository) {
- owner, err := db.GetUserByName(c.Params(":username"))
- if err != nil {
- c.NotFoundOrError(err, "get user by name")
- return nil, nil
- }
-
- repo, err := db.GetRepositoryByName(owner.ID, c.Params(":reponame"))
- if err != nil {
- c.NotFoundOrError(err, "get repository by name")
- return nil, nil
- }
-
- return owner, repo
-}
-
-func TriggerTask(c *context.Context) {
- pusherID := c.QueryInt64("pusher")
- branch := c.Query("branch")
- secret := c.Query("secret")
- if len(branch) == 0 || len(secret) == 0 || pusherID <= 0 {
- c.NotFound()
- log.Trace("TriggerTask: branch or secret is empty, or pusher ID is not valid")
- return
- }
- owner, repo := parseOwnerAndRepo(c)
- if c.Written() {
- return
- }
- if secret != tool.MD5(owner.Salt) {
- c.NotFound()
- log.Trace("TriggerTask [%s/%s]: invalid secret", owner.Name, repo.Name)
- return
- }
-
- pusher, err := db.GetUserByID(pusherID)
- if err != nil {
- c.NotFoundOrError(err, "get user by ID")
- return
- }
-
- log.Trace("TriggerTask '%s/%s' by '%s'", repo.Name, branch, pusher.Name)
-
- go db.HookQueue.Add(repo.ID)
- go db.AddTestPullRequestTask(pusher, repo.ID, branch, true)
- c.Status(202)
-}
diff --git a/internal/route/repo/repo.go b/internal/route/repo/repo.go
index c55314d3b..aa949930f 100644
--- a/internal/route/repo/repo.go
+++ b/internal/route/repo/repo.go
@@ -93,12 +93,9 @@ func handleCreateError(c *context.Context, owner *db.User, err error, name, tpl
case db.IsErrRepoAlreadyExist(err):
c.Data["Err_RepoName"] = true
c.RenderWithErr(c.Tr("form.repo_name_been_taken"), tpl, form)
- case db.IsErrNameReserved(err):
+ case db.IsErrNameNotAllowed(err):
c.Data["Err_RepoName"] = true
- c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tpl, form)
- case db.IsErrNamePatternNotAllowed(err):
- c.Data["Err_RepoName"] = true
- c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tpl, form)
+ c.RenderWithErr(c.Tr("repo.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), tpl, form)
default:
c.Error(err, name)
}
diff --git a/internal/route/repo/setting.go b/internal/route/repo/setting.go
index 1c4446d00..df2b7bfaf 100644
--- a/internal/route/repo/setting.go
+++ b/internal/route/repo/setting.go
@@ -67,10 +67,8 @@ func SettingsPost(c *context.Context, f form.RepoSetting) {
switch {
case db.IsErrRepoAlreadyExist(err):
c.RenderWithErr(c.Tr("form.repo_name_been_taken"), SETTINGS_OPTIONS, &f)
- case db.IsErrNameReserved(err):
- c.RenderWithErr(c.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), SETTINGS_OPTIONS, &f)
- case db.IsErrNamePatternNotAllowed(err):
- c.RenderWithErr(c.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), SETTINGS_OPTIONS, &f)
+ case db.IsErrNameNotAllowed(err):
+ c.RenderWithErr(c.Tr("repo.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), SETTINGS_OPTIONS, &f)
default:
c.Error(err, "change repository name")
}
@@ -156,6 +154,13 @@ func SettingsPost(c *context.Context, f form.RepoSetting) {
repo.PullsIgnoreWhitespace = f.PullsIgnoreWhitespace
repo.PullsAllowRebase = f.PullsAllowRebase
+ if !repo.EnableWiki || repo.EnableExternalWiki {
+ repo.AllowPublicWiki = false
+ }
+ if !repo.EnableIssues || repo.EnableExternalTracker {
+ repo.AllowPublicIssues = false
+ }
+
if err := db.UpdateRepository(repo, false); err != nil {
c.Error(err, "update repository")
return
@@ -425,7 +430,7 @@ func DeleteCollaboration(c *context.Context) {
c.Flash.Success(c.Tr("repo.settings.remove_collaborator_success"))
}
- c.JSONSuccess( map[string]interface{}{
+ c.JSONSuccess(map[string]interface{}{
"redirect": c.Repo.RepoLink + "/settings/collaboration",
})
}
@@ -513,7 +518,7 @@ func SettingsProtectedBranch(c *context.Context) {
c.Data["Users"] = users
c.Data["whitelist_users"] = protectBranch.WhitelistUserIDs
- teams, err := c.Repo.Owner.TeamsHaveAccessToRepo(c.Repo.Repository.ID, db.ACCESS_MODE_WRITE)
+ teams, err := c.Repo.Owner.TeamsHaveAccessToRepo(c.Repo.Repository.ID, db.AccessModeWrite)
if err != nil {
c.Error(err, "get teams have access to the repository")
return
@@ -678,7 +683,7 @@ func DeleteDeployKey(c *context.Context) {
c.Flash.Success(c.Tr("repo.settings.deploy_key_deletion_success"))
}
- c.JSONSuccess( map[string]interface{}{
+ c.JSONSuccess(map[string]interface{}{
"redirect": c.Repo.RepoLink + "/settings/keys",
})
}
diff --git a/internal/route/repo/tasks.go b/internal/route/repo/tasks.go
new file mode 100644
index 000000000..81e85e2aa
--- /dev/null
+++ b/internal/route/repo/tasks.go
@@ -0,0 +1,74 @@
+// 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 (
+ "net/http"
+
+ "gopkg.in/macaron.v1"
+ log "unknwon.dev/clog/v2"
+
+ "gogs.io/gogs/internal/cryptoutil"
+ "gogs.io/gogs/internal/db"
+)
+
+func TriggerTask(c *macaron.Context) {
+ branch := c.Query("branch")
+ pusherID := c.QueryInt64("pusher")
+ secret := c.Query("secret")
+ if branch == "" || pusherID <= 0 || secret == "" {
+ c.Error(http.StatusBadRequest, "Incomplete branch, pusher or secret")
+ return
+ }
+
+ username := c.Params(":username")
+ reponame := c.Params(":reponame")
+
+ owner, err := db.Users.GetByUsername(username)
+ if err != nil {
+ if db.IsErrUserNotExist(err) {
+ c.Error(http.StatusBadRequest, "Owner does not exist")
+ } else {
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get user [name: %s]: %v", username, err)
+ }
+ return
+ }
+
+ // 🚨 SECURITY: No need to check existence of the repository if the client
+ // can't even get the valid secret. Mostly likely not a legitimate request.
+ if secret != cryptoutil.MD5(owner.Salt) {
+ c.Error(http.StatusBadRequest, "Invalid secret")
+ return
+ }
+
+ repo, err := db.Repos.GetByName(owner.ID, reponame)
+ if err != nil {
+ if db.IsErrRepoNotExist(err) {
+ c.Error(http.StatusBadRequest, "Repository does not exist")
+ } else {
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get repository [owner_id: %d, name: %s]: %v", owner.ID, reponame, err)
+ }
+ return
+ }
+
+ pusher, err := db.Users.GetByID(pusherID)
+ if err != nil {
+ if db.IsErrUserNotExist(err) {
+ c.Error(http.StatusBadRequest, "Pusher does not exist")
+ } else {
+ c.Status(http.StatusInternalServerError)
+ log.Error("Failed to get user [id: %d]: %v", pusherID, err)
+ }
+ return
+ }
+
+ log.Trace("TriggerTask: %s/%s@%s by %q", owner.Name, repo.Name, branch, pusher.Name)
+
+ go db.HookQueue.Add(repo.ID)
+ go db.AddTestPullRequestTask(pusher, repo.ID, branch, true)
+ c.Status(http.StatusAccepted)
+}
diff --git a/internal/route/repo/webhook_test.go b/internal/route/repo/webhook_test.go
index 02f27b92a..182c6eed7 100644
--- a/internal/route/repo/webhook_test.go
+++ b/internal/route/repo/webhook_test.go
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"gogs.io/gogs/internal/db"
- "gogs.io/gogs/internal/mock"
+ "gogs.io/gogs/internal/mocks"
)
func Test_isLocalHostname(t *testing.T) {
@@ -33,9 +33,12 @@ func Test_isLocalHostname(t *testing.T) {
}
func Test_validateWebhook(t *testing.T) {
- l := mock.NewLocale("en", func(s string, _ ...interface{}) string {
- return s
- })
+ l := &mocks.Locale{
+ MockLang: "en",
+ MockTr: func(s string, _ ...interface{}) string {
+ return s
+ },
+ }
tests := []struct {
name string
diff --git a/internal/route/user/auth.go b/internal/route/user/auth.go
index a29b63116..143d72bc8 100644
--- a/internal/route/user/auth.go
+++ b/internal/route/user/auth.go
@@ -9,12 +9,12 @@ import (
"net/url"
"github.com/go-macaron/captcha"
+ "github.com/pkg/errors"
log "unknwon.dev/clog/v2"
"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/email"
"gogs.io/gogs/internal/form"
"gogs.io/gogs/internal/tool"
@@ -101,7 +101,7 @@ func Login(c *context.Context) {
}
// Display normal login page
- loginSources, err := db.ActivatedLoginSources()
+ loginSources, err := db.LoginSources.List(db.ListLoginSourceOpts{OnlyActivated: true})
if err != nil {
c.Error(err, "list activated login sources")
return
@@ -148,7 +148,7 @@ func afterLogin(c *context.Context, u *db.User, remember bool) {
func LoginPost(c *context.Context, f form.SignIn) {
c.Title("sign_in")
- loginSources, err := db.ActivatedLoginSources()
+ loginSources, err := db.LoginSources.List(db.ListLoginSourceOpts{OnlyActivated: true})
if err != nil {
c.Error(err, "list activated login sources")
return
@@ -160,13 +160,13 @@ func LoginPost(c *context.Context, f form.SignIn) {
return
}
- u, err := db.UserLogin(f.UserName, f.Password, f.LoginSource)
+ u, err := db.Users.Authenticate(f.UserName, f.Password, f.LoginSource)
if err != nil {
- switch err.(type) {
+ switch errors.Cause(err).(type) {
case db.ErrUserNotExist:
c.FormErr("UserName", "Password")
c.RenderWithErr(c.Tr("form.username_password_incorrect"), LOGIN, &f)
- case errors.LoginSourceMismatch:
+ case db.ErrLoginSourceMismatch:
c.FormErr("LoginSource")
c.RenderWithErr(c.Tr("form.auth_source_mismatch"), LOGIN, &f)
@@ -209,7 +209,7 @@ func LoginTwoFactorPost(c *context.Context) {
return
}
- t, err := db.GetTwoFactorByUserID(userID)
+ t, err := db.TwoFactors.GetByUserID(userID)
if err != nil {
c.Error(err, "get two factor by user ID")
return
@@ -263,7 +263,7 @@ func LoginTwoFactorRecoveryCodePost(c *context.Context) {
}
if err := db.UseRecoveryCode(userID, c.Query("recovery_code")); err != nil {
- if errors.IsTwoFactorRecoveryCodeNotFound(err) {
+ if db.IsTwoFactorRecoveryCodeNotFound(err) {
c.Flash.Error(c.Tr("auth.login_two_factor_invalid_recovery_code"))
c.RedirectSubpath("/user/login/two_factor_recovery_code")
} else {
@@ -344,12 +344,9 @@ func SignUpPost(c *context.Context, cpt *captcha.Captcha, f form.Register) {
case db.IsErrEmailAlreadyUsed(err):
c.FormErr("Email")
c.RenderWithErr(c.Tr("form.email_been_used"), SIGNUP, &f)
- case db.IsErrNameReserved(err):
+ case db.IsErrNameNotAllowed(err):
c.FormErr("UserName")
- c.RenderWithErr(c.Tr("user.form.name_reserved", err.(db.ErrNameReserved).Name), SIGNUP, &f)
- case db.IsErrNamePatternNotAllowed(err):
- c.FormErr("UserName")
- c.RenderWithErr(c.Tr("user.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), SIGNUP, &f)
+ c.RenderWithErr(c.Tr("user.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value()), SIGNUP, &f)
default:
c.Error(err, "create user")
}
@@ -556,7 +553,7 @@ func ResetPasswdPost(c *context.Context) {
c.Error(err, "get user salt")
return
}
- u.EncodePasswd()
+ u.EncodePassword()
if err := db.UpdateUser(u); err != nil {
c.Error(err, "update user")
return
diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go
index c9ccdc8f5..2da64f745 100644
--- a/internal/route/user/setting.go
+++ b/internal/route/user/setting.go
@@ -20,6 +20,7 @@ import (
"gogs.io/gogs/internal/conf"
"gogs.io/gogs/internal/context"
+ "gogs.io/gogs/internal/cryptoutil"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/db/errors"
"gogs.io/gogs/internal/email"
@@ -75,10 +76,8 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
switch {
case db.IsErrUserAlreadyExist(err):
msg = c.Tr("form.username_been_taken")
- case db.IsErrNameReserved(err):
- msg = c.Tr("form.name_reserved")
- case db.IsErrNamePatternNotAllowed(err):
- msg = c.Tr("form.name_pattern_not_allowed")
+ case db.IsErrNameNotAllowed(err):
+ msg = c.Tr("user.form.name_not_allowed", err.(db.ErrNameNotAllowed).Value())
default:
c.Error(err, "change user name")
return
@@ -118,7 +117,7 @@ func SettingsPost(c *context.Context, f form.UpdateProfile) {
func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *db.User) error {
ctxUser.UseCustomAvatar = f.Source == form.AVATAR_LOCAL
if len(f.Gravatar) > 0 {
- ctxUser.Avatar = tool.MD5(f.Gravatar)
+ ctxUser.Avatar = cryptoutil.MD5(f.Gravatar)
ctxUser.AvatarEmail = f.Gravatar
}
@@ -208,7 +207,7 @@ func SettingsPasswordPost(c *context.Context, f form.ChangePassword) {
c.Errorf(err, "get user salt")
return
}
- c.User.EncodePasswd()
+ c.User.EncodePassword()
if err := db.UpdateUser(c.User); err != nil {
c.Errorf(err, "update user")
return
@@ -381,8 +380,8 @@ func SettingsSecurity(c *context.Context) {
c.Title("settings.security")
c.PageIs("SettingsSecurity")
- t, err := db.GetTwoFactorByUserID(c.UserID())
- if err != nil && !errors.IsTwoFactorNotFound(err) {
+ t, err := db.TwoFactors.GetByUserID(c.UserID())
+ if err != nil && !db.IsErrTwoFactorNotFound(err) {
c.Errorf(err, "get two factor by user ID")
return
}
@@ -449,7 +448,7 @@ func SettingsTwoFactorEnablePost(c *context.Context) {
return
}
- if err := db.NewTwoFactor(c.UserID(), secret); err != nil {
+ if err := db.TwoFactors.Create(c.UserID(), conf.Security.SecretKey, secret); err != nil {
c.Flash.Error(c.Tr("settings.two_factor_enable_error", err))
c.RedirectSubpath("/user/settings/security/two_factor_enable")
return
@@ -581,7 +580,7 @@ func SettingsApplications(c *context.Context) {
c.Title("settings.applications")
c.PageIs("SettingsApplications")
- tokens, err := db.ListAccessTokens(c.User.ID)
+ tokens, err := db.AccessTokens.List(c.User.ID)
if err != nil {
c.Errorf(err, "list access tokens")
return
@@ -596,7 +595,7 @@ func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) {
c.PageIs("SettingsApplications")
if c.HasError() {
- tokens, err := db.ListAccessTokens(c.User.ID)
+ tokens, err := db.AccessTokens.List(c.User.ID)
if err != nil {
c.Errorf(err, "list access tokens")
return
@@ -607,12 +606,9 @@ func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) {
return
}
- t := &db.AccessToken{
- UID: c.User.ID,
- Name: f.Name,
- }
- if err := db.NewAccessToken(t); err != nil {
- if errors.IsAccessTokenNameAlreadyExist(err) {
+ t, err := db.AccessTokens.Create(c.User.ID, f.Name)
+ if err != nil {
+ if db.IsErrAccessTokenAlreadyExist(err) {
c.Flash.Error(c.Tr("settings.token_name_exists"))
c.RedirectSubpath("/user/settings/applications")
} else {
@@ -627,7 +623,7 @@ func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) {
}
func SettingsDeleteApplication(c *context.Context) {
- if err := db.DeleteAccessTokenOfUserByID(c.User.ID, c.QueryInt64("id")); err != nil {
+ if err := db.AccessTokens.DeleteByID(c.User.ID, c.QueryInt64("id")); err != nil {
c.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
c.Flash.Success(c.Tr("settings.delete_token_success"))
@@ -643,7 +639,7 @@ func SettingsDelete(c *context.Context) {
c.PageIs("SettingsDelete")
if c.Req.Method == "POST" {
- if _, err := db.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
+ if _, err := db.Users.Authenticate(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil {
if db.IsErrUserNotExist(err) {
c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SETTINGS_DELETE, nil)
} else {
diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go
index a4d41d0f6..ff9fb8cc9 100644
--- a/internal/ssh/ssh.go
+++ b/internal/ssh/ssh.go
@@ -51,17 +51,27 @@ func handleServerConn(keyID string, chans <-chan ssh.NewChannel) {
payload := cleanCommand(string(req.Payload))
switch req.Type {
case "env":
- args := strings.Split(strings.Replace(payload, "\x00", "", -1), "\v")
- if len(args) != 2 {
- log.Warn("SSH: Invalid env arguments: '%#v'", args)
+ var env struct {
+ Name string
+ Value string
+ }
+ if err := ssh.Unmarshal(req.Payload, &env); err != nil {
+ log.Warn("SSH: Invalid env payload %q: %v", req.Payload, err)
+ continue
+ }
+ // Sometimes the client could send malformed command (i.e. missing "="),
+ // see https://discuss.gogs.io/t/ssh/3106.
+ if env.Name == "" || env.Value == "" {
+ log.Warn("SSH: Invalid env arguments: %+v", env)
continue
}
- args[0] = strings.TrimLeft(args[0], "\x04")
- _, _, err := com.ExecCmdBytes("env", args[0]+"="+args[1])
+
+ _, stderr, err := com.ExecCmd("env", fmt.Sprintf("%s=%s", env.Name, env.Value))
if err != nil {
- log.Error("env: %v", err)
+ log.Error("env: %v - %s", err, stderr)
return
}
+
case "exec":
cmdName := strings.TrimLeft(payload, "'()")
log.Trace("SSH: Payload: %v", cmdName)
diff --git a/internal/strutil/strutil.go b/internal/strutil/strutil.go
new file mode 100644
index 000000000..b1de241f5
--- /dev/null
+++ b/internal/strutil/strutil.go
@@ -0,0 +1,46 @@
+// 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 strutil
+
+import (
+ "crypto/rand"
+ "math/big"
+ "unicode"
+)
+
+// ToUpperFirst returns s with only the first Unicode letter mapped to its upper case.
+func ToUpperFirst(s string) string {
+ for i, v := range s {
+ return string(unicode.ToUpper(v)) + s[i+1:]
+ }
+ return ""
+}
+
+// RandomChars returns a generated string in given number of random characters.
+func RandomChars(n int) (string, error) {
+ const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+
+ randomInt := func(max *big.Int) (int, error) {
+ r, err := rand.Int(rand.Reader, max)
+ if err != nil {
+ return 0, err
+ }
+
+ return int(r.Int64()), nil
+ }
+
+ buffer := make([]byte, n)
+ max := big.NewInt(int64(len(alphanum)))
+ for i := 0; i < n; i++ {
+ index, err := randomInt(max)
+ if err != nil {
+ return "", err
+ }
+
+ buffer[i] = alphanum[index]
+ }
+
+ return string(buffer), nil
+}
diff --git a/internal/strutil/strutil_test.go b/internal/strutil/strutil_test.go
new file mode 100644
index 000000000..c4edf1400
--- /dev/null
+++ b/internal/strutil/strutil_test.go
@@ -0,0 +1,57 @@
+// 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 strutil
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToUpperFirst(t *testing.T) {
+ tests := []struct {
+ name string
+ s string
+ expStr string
+ }{
+ {
+ name: "empty string",
+ },
+ {
+ name: "first letter is a digit",
+ s: "123 let's go",
+ expStr: "123 let's go",
+ },
+ {
+ name: "lower to upper",
+ s: "this is a sentence",
+ expStr: "This is a sentence",
+ },
+ {
+ name: "already in upper case",
+ s: "Let's go",
+ expStr: "Let's go",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expStr, ToUpperFirst(test.s))
+ })
+ }
+}
+
+func TestRandomChars(t *testing.T) {
+ cache := make(map[string]bool)
+ for i := 0; i < 100; i++ {
+ chars, err := RandomChars(10)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cache[chars] {
+ t.Fatalf("Duplicated chars %q", chars)
+ }
+ cache[chars] = true
+ }
+}
diff --git a/internal/testutil/golden.go b/internal/testutil/golden.go
index fa584d1ba..15ac095fc 100644
--- a/internal/testutil/golden.go
+++ b/internal/testutil/golden.go
@@ -9,6 +9,7 @@ import (
"flag"
"io/ioutil"
"regexp"
+ "runtime"
"testing"
"github.com/stretchr/testify/assert"
@@ -25,8 +26,13 @@ func Update(name string) bool {
}
// AssertGolden compares what's got and what's in the golden file. It updates
-// the golden file on-demand.
+// the golden file on-demand. It does nothing when the runtime is "windows".
func AssertGolden(t testing.TB, path string, update bool, got interface{}) {
+ if runtime.GOOS == "windows" {
+ t.Skip("Skipping testing on Windows")
+ return
+ }
+
t.Helper()
data := marshal(t, got)
diff --git a/internal/testutil/noop_logger.go b/internal/testutil/noop_logger.go
new file mode 100644
index 000000000..bf536f3f0
--- /dev/null
+++ b/internal/testutil/noop_logger.go
@@ -0,0 +1,31 @@
+// 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 testutil
+
+import (
+ log "unknwon.dev/clog/v2"
+)
+
+var _ log.Logger = (*noopLogger)(nil)
+
+// noopLogger is a placeholder logger that logs nothing.
+type noopLogger struct{}
+
+func (l *noopLogger) Name() string {
+ return "noop"
+}
+
+func (l *noopLogger) Level() log.Level {
+ return log.LevelTrace
+}
+
+func (l *noopLogger) Write(log.Messager) error {
+ return nil
+}
+
+// InitNoopLogger is a init function to initialize a noop logger.
+var InitNoopLogger = func(name string, vs ...interface{}) (log.Logger, error) {
+ return &noopLogger{}, nil
+}
diff --git a/internal/tool/path_test.go b/internal/tool/path_test.go
index 44ee975f2..be9b5192a 100644
--- a/internal/tool/path_test.go
+++ b/internal/tool/path_test.go
@@ -7,47 +7,47 @@ package tool
import (
"testing"
- . "github.com/smartystreets/goconvey/convey"
+ "github.com/stretchr/testify/assert"
)
func Test_IsSameSiteURLPath(t *testing.T) {
- Convey("Check if a path belongs to the same site", t, func() {
- testCases := []struct {
- url string
- expect bool
- }{
- {"//github.com", false},
- {"http://github.com", false},
- {"https://github.com", false},
- {"/\\github.com", false},
-
- {"/admin", true},
- {"/user/repo", true},
- }
-
- for _, tc := range testCases {
- So(IsSameSiteURLPath(tc.url), ShouldEqual, tc.expect)
- }
- })
+ tests := []struct {
+ url string
+ expVal bool
+ }{
+ {url: "//github.com", expVal: false},
+ {url: "http://github.com", expVal: false},
+ {url: "https://github.com", expVal: false},
+ {url: "/\\github.com", expVal: false},
+
+ {url: "/admin", expVal: true},
+ {url: "/user/repo", expVal: true},
+ }
+
+ for _, test := range tests {
+ t.Run(test.url, func(t *testing.T) {
+ assert.Equal(t, test.expVal, IsSameSiteURLPath(test.url))
+ })
+ }
}
func Test_IsMaliciousPath(t *testing.T) {
- Convey("Detects malicious path", t, func() {
- testCases := []struct {
- path string
- expect bool
- }{
- {"../../../../../../../../../data/gogs/data/sessions/a/9/a9f0ab6c3ef63dd8", true},
- {"..\\/..\\/../data/gogs/data/sessions/a/9/a9f0ab6c3ef63dd8", true},
- {"data/gogs/../../../../../../../../../data/sessions/a/9/a9f0ab6c3ef63dd8", true},
- {"..\\..\\..\\..\\..\\..\\..\\..\\..\\data\\gogs\\data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", true},
- {"data\\gogs\\..\\..\\..\\..\\..\\..\\..\\..\\..\\data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", true},
-
- {"data/sessions/a/9/a9f0ab6c3ef63dd8", false},
- {"data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", false},
- }
- for _, tc := range testCases {
- So(IsMaliciousPath(tc.path), ShouldEqual, tc.expect)
- }
- })
+ tests := []struct {
+ path string
+ expVal bool
+ }{
+ {path: "../../../../../../../../../data/gogs/data/sessions/a/9/a9f0ab6c3ef63dd8", expVal: true},
+ {path: "..\\/..\\/../data/gogs/data/sessions/a/9/a9f0ab6c3ef63dd8", expVal: true},
+ {path: "data/gogs/../../../../../../../../../data/sessions/a/9/a9f0ab6c3ef63dd8", expVal: true},
+ {path: "..\\..\\..\\..\\..\\..\\..\\..\\..\\data\\gogs\\data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", expVal: true},
+ {path: "data\\gogs\\..\\..\\..\\..\\..\\..\\..\\..\\..\\data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", expVal: true},
+
+ {path: "data/sessions/a/9/a9f0ab6c3ef63dd8", expVal: false},
+ {path: "data\\sessions\\a\\9\\a9f0ab6c3ef63dd8", expVal: false},
+ }
+ for _, test := range tests {
+ t.Run(test.path, func(t *testing.T) {
+ assert.Equal(t, test.expVal, IsMaliciousPath(test.path))
+ })
+ }
}
diff --git a/internal/tool/tool.go b/internal/tool/tool.go
index 380f1069b..e58250187 100644
--- a/internal/tool/tool.go
+++ b/internal/tool/tool.go
@@ -6,13 +6,11 @@ package tool
import (
"crypto/md5"
- "crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"html/template"
- "math/big"
"strings"
"time"
"unicode"
@@ -27,19 +25,8 @@ import (
"gogs.io/gogs/internal/conf"
)
-// MD5Bytes encodes string to MD5 bytes.
-func MD5Bytes(str string) []byte {
- m := md5.New()
- _, _ = m.Write([]byte(str))
- return m.Sum(nil)
-}
-
-// MD5 encodes string to MD5 hex value.
-func MD5(str string) string {
- return hex.EncodeToString(MD5Bytes(str))
-}
-
// SHA1 encodes string to SHA1 hex value.
+// TODO: Move to cryptoutil.SHA1.
func SHA1(str string) string {
h := sha1.New()
_, _ = h.Write([]byte(str))
@@ -83,40 +70,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
return auth[0], auth[1], nil
}
-// BasicAuthEncode encodes username and password in HTTP Basic Authentication format.
-func BasicAuthEncode(username, password string) string {
- return base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
-}
-
-const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-
-// RandomString returns generated random string in given length of characters.
-// It also returns possible error during generation.
-func RandomString(n int) (string, error) {
- buffer := make([]byte, n)
- max := big.NewInt(int64(len(alphanum)))
-
- for i := 0; i < n; i++ {
- index, err := randomInt(max)
- if err != nil {
- return "", err
- }
-
- buffer[i] = alphanum[index]
- }
-
- return string(buffer), nil
-}
-
-func randomInt(max *big.Int) (int, error) {
- rand, err := rand.Int(rand.Reader, max)
- if err != nil {
- return 0, err
- }
-
- return int(rand.Int64()), nil
-}
-
// verify time limit code
func VerifyTimeLimitCode(data string, minutes int, code string) bool {
if len(code) <= 18 {
diff --git a/public/css/github.min.css b/public/css/github.min.css
deleted file mode 100644
index 7b3600c93..000000000
--- a/public/css/github.min.css
+++ /dev/null
@@ -1 +0,0 @@
-.hljs{display:block;overflow-x:auto;padding:.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-template_comment,.diff .hljs-header,.hljs-javadoc{color:#998;font-style:italic}.hljs-keyword,.css .rule .hljs-keyword,.hljs-winutils,.javascript .hljs-title,.nginx .hljs-title,.hljs-subst,.hljs-request,.hljs-status{color:#333;font-weight:bold}.hljs-number,.hljs-hexcolor,.ruby .hljs-constant{color:#099}.hljs-string,.hljs-tag .hljs-value,.hljs-phpdoc,.tex .hljs-formula{color:#d14}.hljs-title,.hljs-id,.coffeescript .hljs-params,.scss .hljs-preprocessor{color:#900;font-weight:bold}.javascript .hljs-title,.lisp .hljs-title,.clojure .hljs-title,.hljs-subst{font-weight:normal}.hljs-class .hljs-title,.haskell .hljs-type,.vhdl .hljs-literal,.tex .hljs-command{color:#458;font-weight:bold}.hljs-tag,.hljs-tag .hljs-title,.hljs-rules .hljs-property,.django .hljs-tag .hljs-keyword{color:#000080;font-weight:normal}.hljs-attribute,.hljs-variable,.lisp .hljs-body{color:#008080}.hljs-regexp{color:#009926}.hljs-symbol,.ruby .hljs-symbol .hljs-string,.lisp .hljs-keyword,.tex .hljs-special,.hljs-prompt{color:#990073}.hljs-built_in,.lisp .hljs-title,.clojure .hljs-built_in{color:#0086b3}.hljs-preprocessor,.hljs-pragma,.hljs-pi,.hljs-doctype,.hljs-shebang,.hljs-cdata{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa}
\ No newline at end of file
diff --git a/public/js/gogs.js b/public/js/gogs.js
index d04008c59..6ce559584 100644
--- a/public/js/gogs.js
+++ b/public/js/gogs.js
@@ -1,1498 +1,1798 @@
-'use strict';
+"use strict";
var csrf;
var suburl;
function initCommentPreviewTab($form) {
- var $tabMenu = $form.find('.tabular.menu');
- $tabMenu.find('.item').tab();
- $tabMenu.find('.item[data-tab="' + $tabMenu.data('preview') + '"]').click(function () {
- var $this = $(this);
- $.post($this.data('url'), {
- "_csrf": csrf,
- "mode": "gfm",
- "context": $this.data('context'),
- "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
+ var $tabMenu = $form.find(".tabular.menu");
+ $tabMenu.find(".item").tab();
+ $tabMenu
+ .find('.item[data-tab="' + $tabMenu.data("preview") + '"]')
+ .click(function() {
+ var $this = $(this);
+ $.post(
+ $this.data("url"),
+ {
+ _csrf: csrf,
+ mode: "gfm",
+ context: $this.data("context"),
+ text: $form
+ .find(
+ '.tab.segment[data-tab="' + $tabMenu.data("write") + '"] textarea'
+ )
+ .val()
},
- function (data) {
- var $previewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('preview') + '"]');
- $previewPanel.html(data);
- emojify.run($previewPanel[0]);
- $('pre code', $previewPanel[0]).each(function (i, block) {
- hljs.highlightBlock(block);
- });
- }
- );
+ function(data) {
+ var $previewPanel = $form.find(
+ '.tab.segment[data-tab="' + $tabMenu.data("preview") + '"]'
+ );
+ $previewPanel.html(data);
+ emojify.run($previewPanel[0]);
+ $("pre code", $previewPanel[0]).each(function(i, block) {
+ hljs.highlightBlock(block);
+ });
+ }
+ );
});
- buttonsClickOnEnter();
+ buttonsClickOnEnter();
}
var previewFileModes;
function initEditPreviewTab($form) {
- var $tabMenu = $form.find('.tabular.menu');
- $tabMenu.find('.item').tab();
- var $previewTab = $tabMenu.find('.item[data-tab="' + $tabMenu.data('preview') + '"]');
- if ($previewTab.length) {
- previewFileModes = $previewTab.data('preview-file-modes').split(',');
- $previewTab.click(function () {
- var $this = $(this);
- $.post($this.data('url'), {
- "_csrf": csrf,
- "context": $this.data('context'),
- "text": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
- },
- function (data) {
- var $previewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('preview') + '"]');
- $previewPanel.html(data);
- emojify.run($previewPanel[0]);
- $('pre code', $previewPanel[0]).each(function (i, block) {
- hljs.highlightBlock(block);
- });
- }
- );
- });
- }
+ var $tabMenu = $form.find(".tabular.menu");
+ $tabMenu.find(".item").tab();
+ var $previewTab = $tabMenu.find(
+ '.item[data-tab="' + $tabMenu.data("preview") + '"]'
+ );
+ if ($previewTab.length) {
+ previewFileModes = $previewTab.data("preview-file-modes").split(",");
+ $previewTab.click(function() {
+ var $this = $(this);
+ $.post(
+ $this.data("url"),
+ {
+ _csrf: csrf,
+ context: $this.data("context"),
+ text: $form
+ .find(
+ '.tab.segment[data-tab="' + $tabMenu.data("write") + '"] textarea'
+ )
+ .val()
+ },
+ function(data) {
+ var $previewPanel = $form.find(
+ '.tab.segment[data-tab="' + $tabMenu.data("preview") + '"]'
+ );
+ $previewPanel.html(data);
+ emojify.run($previewPanel[0]);
+ $("pre code", $previewPanel[0]).each(function(i, block) {
+ hljs.highlightBlock(block);
+ });
+ }
+ );
+ });
+ }
}
function initEditDiffTab($form) {
- var $tabMenu = $form.find('.tabular.menu');
- $tabMenu.find('.item').tab();
- $tabMenu.find('.item[data-tab="' + $tabMenu.data('diff') + '"]').click(function () {
- var $this = $(this);
- $.post($this.data('url'), {
- "_csrf": csrf,
- "content": $form.find('.tab.segment[data-tab="' + $tabMenu.data('write') + '"] textarea').val()
+ var $tabMenu = $form.find(".tabular.menu");
+ $tabMenu.find(".item").tab();
+ $tabMenu
+ .find('.item[data-tab="' + $tabMenu.data("diff") + '"]')
+ .click(function() {
+ var $this = $(this);
+ $.post(
+ $this.data("url"),
+ {
+ _csrf: csrf,
+ content: $form
+ .find(
+ '.tab.segment[data-tab="' + $tabMenu.data("write") + '"] textarea'
+ )
+ .val()
},
- function (data) {
- var $diffPreviewPanel = $form.find('.tab.segment[data-tab="' + $tabMenu.data('diff') + '"]');
- $diffPreviewPanel.html(data);
- emojify.run($diffPreviewPanel[0]);
- }
- );
+ function(data) {
+ var $diffPreviewPanel = $form.find(
+ '.tab.segment[data-tab="' + $tabMenu.data("diff") + '"]'
+ );
+ $diffPreviewPanel.html(data);
+ emojify.run($diffPreviewPanel[0]);
+ }
+ );
});
}
-
function initEditForm() {
- if ($('.edit.form').length == 0) {
- return;
- }
+ if ($(".edit.form").length == 0) {
+ return;
+ }
- initEditPreviewTab($('.edit.form'));
- initEditDiffTab($('.edit.form'));
+ initEditPreviewTab($(".edit.form"));
+ initEditDiffTab($(".edit.form"));
}
-
function initCommentForm() {
- if ($('.comment.form').length == 0) {
- return
- }
-
- initCommentPreviewTab($('.comment.form'));
-
- // Labels
- var $list = $('.ui.labels.list');
- var $noSelect = $list.find('.no-select');
- var $labelMenu = $('.select-label .menu');
- var hasLabelUpdateAction = $labelMenu.data('action') == 'update';
-
- function updateIssueMeta(url, action, id) {
- $.post(url, {
- "_csrf": csrf,
- "action": action,
- "id": id
- });
+ if ($(".comment.form").length == 0) {
+ return;
+ }
+
+ initCommentPreviewTab($(".comment.form"));
+
+ // Labels
+ var $list = $(".ui.labels.list");
+ var $noSelect = $list.find(".no-select");
+ var $labelMenu = $(".select-label .menu");
+ var hasLabelUpdateAction = $labelMenu.data("action") == "update";
+
+ function updateIssueMeta(url, action, id) {
+ $.post(url, {
+ _csrf: csrf,
+ action: action,
+ id: id
+ });
+ }
+
+ // Add to each unselected label to keep UI looks good.
+ // This should be added directly to HTML but somehow just get empty on this page.
+ $labelMenu
+ .find(".item:not(.no-select) .octicon:not(.octicon-check)")
+ .each(function() {
+ $(this).html(" ");
+ });
+ $labelMenu.find(".item:not(.no-select)").click(function() {
+ if ($(this).hasClass("checked")) {
+ $(this).removeClass("checked");
+ $(this)
+ .find(".octicon")
+ .removeClass("octicon-check")
+ .html(" ");
+ if (hasLabelUpdateAction) {
+ updateIssueMeta(
+ $labelMenu.data("update-url"),
+ "detach",
+ $(this).data("id")
+ );
+ }
+ } else {
+ $(this).addClass("checked");
+ $(this)
+ .find(".octicon")
+ .addClass("octicon-check")
+ .html("");
+ if (hasLabelUpdateAction) {
+ updateIssueMeta(
+ $labelMenu.data("update-url"),
+ "attach",
+ $(this).data("id")
+ );
+ }
}
- // Add to each unselected label to keep UI looks good.
- // This should be added directly to HTML but somehow just get empty on this page.
- $labelMenu.find('.item:not(.no-select) .octicon:not(.octicon-check)').each(function () {
- $(this).html(' ');
- });
- $labelMenu.find('.item:not(.no-select)').click(function () {
- if ($(this).hasClass('checked')) {
- $(this).removeClass('checked');
- $(this).find('.octicon').removeClass('octicon-check').html(' ');
- if (hasLabelUpdateAction) {
- updateIssueMeta($labelMenu.data('update-url'), "detach", $(this).data('id'));
- }
+ var labelIds = "";
+ $(this)
+ .parent()
+ .find(".item")
+ .each(function() {
+ if ($(this).hasClass("checked")) {
+ labelIds += $(this).data("id") + ",";
+ $($(this).data("id-selector")).removeClass("hide");
} else {
- $(this).addClass('checked');
- $(this).find('.octicon').addClass('octicon-check').html('');
- if (hasLabelUpdateAction) {
- updateIssueMeta($labelMenu.data('update-url'), "attach", $(this).data('id'));
- }
+ $($(this).data("id-selector")).addClass("hide");
}
+ });
+ if (labelIds.length == 0) {
+ $noSelect.removeClass("hide");
+ } else {
+ $noSelect.addClass("hide");
+ }
+ $(
+ $(this)
+ .parent()
+ .data("id")
+ ).val(labelIds);
+ return false;
+ });
+ $labelMenu.find(".no-select.item").click(function() {
+ if (hasLabelUpdateAction) {
+ updateIssueMeta($labelMenu.data("update-url"), "clear", "");
+ }
- var labelIds = "";
- $(this).parent().find('.item').each(function () {
- if ($(this).hasClass('checked')) {
- labelIds += $(this).data('id') + ",";
- $($(this).data('id-selector')).removeClass('hide');
- } else {
- $($(this).data('id-selector')).addClass('hide');
- }
- });
- if (labelIds.length == 0) {
- $noSelect.removeClass('hide');
- } else {
- $noSelect.addClass('hide');
- }
- $($(this).parent().data('id')).val(labelIds);
- return false;
+ $(this)
+ .parent()
+ .find(".item")
+ .each(function() {
+ $(this).removeClass("checked");
+ $(this)
+ .find(".octicon")
+ .removeClass("octicon-check")
+ .html(" ");
+ });
+
+ $list.find(".item").each(function() {
+ $(this).addClass("hide");
});
- $labelMenu.find('.no-select.item').click(function () {
- if (hasLabelUpdateAction) {
- updateIssueMeta($labelMenu.data('update-url'), "clear", '');
- }
-
- $(this).parent().find('.item').each(function () {
- $(this).removeClass('checked');
- $(this).find('.octicon').removeClass('octicon-check').html(' ');
+ $noSelect.removeClass("hide");
+ $(
+ $(this)
+ .parent()
+ .data("id")
+ ).val("");
+ });
+
+ function selectItem(select_id, input_id) {
+ var $menu = $(select_id + " .menu");
+ var $list = $(".ui" + select_id + ".list");
+ var hasUpdateAction = $menu.data("action") == "update";
+
+ $menu.find(".item:not(.no-select)").click(function() {
+ $(this)
+ .parent()
+ .find(".item")
+ .each(function() {
+ $(this).removeClass("selected active");
});
- $list.find('.item').each(function () {
- $(this).addClass('hide');
- });
- $noSelect.removeClass('hide');
- $($(this).parent().data('id')).val('');
+ $(this).addClass("selected active");
+ if (hasUpdateAction) {
+ updateIssueMeta($menu.data("update-url"), "", $(this).data("id"));
+ }
+ switch (input_id) {
+ case "#milestone_id":
+ $list
+ .find(".selected")
+ .html(
+ '' +
- $(this).text() + '');
- break;
- case '#assignee_id':
- $list.find('.selected').html('' +
- '{{.Database.Path}} {{.i18n.Tr "admin.config.db.path_helper"}}{{.LFS.ObjectsPath}}