Merge branch 'master' of https://github.com/go-gitea/gitea
# Conflicts: # models/migrations/migrations.go # models/migrations/v50.go
This commit is contained in:
commit
fae44666ec
|
|
@ -182,6 +182,7 @@ The goal of this project is to make the easiest, fastest, and most painless way
|
|||
- Labels
|
||||
- Assign issues
|
||||
- Track time
|
||||
- Reactions
|
||||
- Filter
|
||||
- Open
|
||||
- Closed
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
|
|||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branches/%s", branchName)
|
||||
resp := session.MakeRequest(t, req, NoExpectedStatus)
|
||||
if !exists {
|
||||
assert.EqualValues(t, http.StatusNotFound, resp.HeaderCode)
|
||||
assert.EqualValues(t, http.StatusNotFound, resp.Code)
|
||||
return
|
||||
}
|
||||
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
|
||||
assert.EqualValues(t, http.StatusOK, resp.Code)
|
||||
var branch api.Branch
|
||||
DecodeJSON(t, resp, &branch)
|
||||
assert.EqualValues(t, branchName, branch.Name)
|
||||
|
|
|
|||
|
|
@ -6,27 +6,30 @@ package integrations
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type makeRequestFunc func(testing.TB, *http.Request, int) *httptest.ResponseRecorder
|
||||
|
||||
func TestGPGKeys(t *testing.T) {
|
||||
prepareTestEnv(t)
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
tt := []struct {
|
||||
name string
|
||||
reqBuilder func(testing.TB, *http.Request, int) *TestResponse
|
||||
results []int
|
||||
name string
|
||||
makeRequest makeRequestFunc
|
||||
results []int
|
||||
}{
|
||||
{name: "NoLogin", reqBuilder: MakeRequest,
|
||||
{name: "NoLogin", makeRequest: MakeRequest,
|
||||
results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
|
||||
},
|
||||
{name: "LoggedAsUser2", reqBuilder: session.MakeRequest,
|
||||
{name: "LoggedAsUser2", makeRequest: session.MakeRequest,
|
||||
results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusInternalServerError, http.StatusInternalServerError, http.StatusCreated, http.StatusCreated}},
|
||||
}
|
||||
|
||||
|
|
@ -35,29 +38,29 @@ func TestGPGKeys(t *testing.T) {
|
|||
//Basic test on result code
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("ViewOwnGPGKeys", func(t *testing.T) {
|
||||
testViewOwnGPGKeys(t, tc.reqBuilder, tc.results[0])
|
||||
testViewOwnGPGKeys(t, tc.makeRequest, tc.results[0])
|
||||
})
|
||||
t.Run("ViewGPGKeys", func(t *testing.T) {
|
||||
testViewGPGKeys(t, tc.reqBuilder, tc.results[1])
|
||||
testViewGPGKeys(t, tc.makeRequest, tc.results[1])
|
||||
})
|
||||
t.Run("GetGPGKey", func(t *testing.T) {
|
||||
testGetGPGKey(t, tc.reqBuilder, tc.results[2])
|
||||
testGetGPGKey(t, tc.makeRequest, tc.results[2])
|
||||
})
|
||||
t.Run("DeleteGPGKey", func(t *testing.T) {
|
||||
testDeleteGPGKey(t, tc.reqBuilder, tc.results[3])
|
||||
testDeleteGPGKey(t, tc.makeRequest, tc.results[3])
|
||||
})
|
||||
|
||||
t.Run("CreateInvalidGPGKey", func(t *testing.T) {
|
||||
testCreateInvalidGPGKey(t, tc.reqBuilder, tc.results[4])
|
||||
testCreateInvalidGPGKey(t, tc.makeRequest, tc.results[4])
|
||||
})
|
||||
t.Run("CreateNoneRegistredEmailGPGKey", func(t *testing.T) {
|
||||
testCreateNoneRegistredEmailGPGKey(t, tc.reqBuilder, tc.results[5])
|
||||
testCreateNoneRegistredEmailGPGKey(t, tc.makeRequest, tc.results[5])
|
||||
})
|
||||
t.Run("CreateValidGPGKey", func(t *testing.T) {
|
||||
testCreateValidGPGKey(t, tc.reqBuilder, tc.results[6])
|
||||
testCreateValidGPGKey(t, tc.makeRequest, tc.results[6])
|
||||
})
|
||||
t.Run("CreateValidSecondaryEmailGPGKey", func(t *testing.T) {
|
||||
testCreateValidSecondaryEmailGPGKey(t, tc.reqBuilder, tc.results[7])
|
||||
testCreateValidSecondaryEmailGPGKey(t, tc.makeRequest, tc.results[7])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -140,39 +143,39 @@ func TestGPGKeys(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func testViewOwnGPGKeys(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
func testViewOwnGPGKeys(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys")
|
||||
reqBuilder(t, req, expected)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testViewGPGKeys(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
func testViewGPGKeys(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user2/gpg_keys")
|
||||
reqBuilder(t, req, expected)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testGetGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
func testGetGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys/1")
|
||||
reqBuilder(t, req, expected)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testDeleteGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
func testDeleteGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
req := NewRequest(t, "DELETE", "/api/v1/user/gpg_keys/1")
|
||||
reqBuilder(t, req, expected)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testCreateGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int, publicKey string) {
|
||||
func testCreateGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int, publicKey string) {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", api.CreateGPGKeyOption{
|
||||
ArmoredKey: publicKey,
|
||||
})
|
||||
reqBuilder(t, req, expected)
|
||||
makeRequest(t, req, expected)
|
||||
}
|
||||
|
||||
func testCreateInvalidGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
testCreateGPGKey(t, reqBuilder, expected, "invalid_key")
|
||||
func testCreateInvalidGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
testCreateGPGKey(t, makeRequest, expected, "invalid_key")
|
||||
}
|
||||
|
||||
func testCreateNoneRegistredEmailGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
testCreateGPGKey(t, reqBuilder, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
func testCreateNoneRegistredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
testCreateGPGKey(t, makeRequest, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmGUygBCACjCNbKvMGgp0fd5vyFW9olE1CLCSyyF9gQN2hSuzmZLuAZF2Kh
|
||||
dCMCG2T1UwzUB/yWUFWJ2BtCwSjuaRv+cGohqEy6bhEBV90peGA33lHfjx7wP25O
|
||||
|
|
@ -191,9 +194,9 @@ INx/MmBfmtCq05FqNclvU+sj2R3N1JJOtBOjZrJHQbJhzoILou8AkxeX1A+q9OAz
|
|||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
}
|
||||
|
||||
func testCreateValidGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
func testCreateValidGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
//User2 <user2@example.com> //primary & activated
|
||||
testCreateGPGKey(t, reqBuilder, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
testCreateGPGKey(t, makeRequest, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmGVsMBCACuxgZ7W7rI9xN08Y4M7B8yx/6/I4Slm94+wXf8YNRvAyqj30dW
|
||||
VJhyBcnfNRDLKSQp5o/hhfDkCgdqBjLa1PnHlGS3PXJc0hP/FyYPD2BFvNMPpCYS
|
||||
|
|
@ -225,9 +228,9 @@ uy6MA3VSB99SK9ducGmE1Jv8mcziREroz2TEGr0zPs6h
|
|||
-----END PGP PUBLIC KEY BLOCK-----`)
|
||||
}
|
||||
|
||||
func testCreateValidSecondaryEmailGPGKey(t *testing.T, reqBuilder func(testing.TB, *http.Request, int) *TestResponse, expected int) {
|
||||
func testCreateValidSecondaryEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, expected int) {
|
||||
//User2 <user21@example.com> //secondary and not activated
|
||||
testCreateGPGKey(t, reqBuilder, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
testCreateGPGKey(t, makeRequest, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmGWN4BCAC18V4tVGO65VLCV7p14FuXJlUtZ5CuYMvgEkcOqrvRaBSW9ao4
|
||||
PGESOhJpfWpnW3QgJniYndLzPpsmdHEclEER6aZjiNgReWPOjHD5tykWocZAJqXD
|
||||
|
|
|
|||
|
|
@ -59,9 +59,8 @@ func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
|
|||
link, exists := htmlDoc.doc.Find(button).Attr("data-url")
|
||||
assert.True(t, exists, "The template has changed")
|
||||
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
req = NewRequestWithValues(t, "POST", link, map[string]string{
|
||||
"_csrf": getCsrf(htmlDoc.doc),
|
||||
"_csrf": getCsrf(t, htmlDoc.doc),
|
||||
})
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
|
@ -73,7 +72,8 @@ func branchAction(t *testing.T, button string) (*HTMLDoc, string) {
|
|||
return NewHTMLParser(t, resp.Body), url.Query()["name"][0]
|
||||
}
|
||||
|
||||
func getCsrf(doc *goquery.Document) string {
|
||||
csrf, _ := doc.Find("meta[name=\"_csrf\"]").Attr("content")
|
||||
func getCsrf(t *testing.T, doc *goquery.Document) string {
|
||||
csrf, exists := doc.Find("meta[name=\"_csrf\"]").Attr("content")
|
||||
assert.True(t, exists)
|
||||
return csrf
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package integrations
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
|
|
@ -72,7 +73,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
|
|||
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
// Check body for error message
|
||||
assert.Contains(t, string(resp.Body), "Can not commit to protected branch 'master'.")
|
||||
assert.Contains(t, resp.Body.String(), "Can not commit to protected branch 'master'.")
|
||||
|
||||
// remove the protected branch
|
||||
csrf = GetCSRF(t, session, "/user2/repo1/settings/branches")
|
||||
|
|
@ -89,7 +90,7 @@ func TestCreateFileOnProtectedBranch(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *TestResponse {
|
||||
func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder {
|
||||
// Get to the 'edit this file' page
|
||||
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
|
@ -113,12 +114,12 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa
|
|||
// Verify the change
|
||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.EqualValues(t, newContent, string(resp.Body))
|
||||
assert.EqualValues(t, newContent, resp.Body.String())
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *TestResponse {
|
||||
func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder {
|
||||
|
||||
// Get to the 'edit this file' page
|
||||
req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath))
|
||||
|
|
@ -144,7 +145,7 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
|
|||
// Verify the change
|
||||
req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.EqualValues(t, newContent, string(resp.Body))
|
||||
assert.EqualValues(t, newContent, resp.Body.String())
|
||||
|
||||
return resp
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ type HTMLDoc struct {
|
|||
}
|
||||
|
||||
// NewHTMLParser parse html file
|
||||
func NewHTMLParser(t testing.TB, content []byte) *HTMLDoc {
|
||||
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(content))
|
||||
func NewHTMLParser(t testing.TB, body *bytes.Buffer) *HTMLDoc {
|
||||
doc, err := goquery.NewDocumentFromReader(body)
|
||||
assert.NoError(t, err)
|
||||
return &HTMLDoc{doc: doc}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
|
@ -158,7 +159,7 @@ func (s *TestSession) GetCookie(name string) *http.Cookie {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *TestSession) MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestResponse {
|
||||
func (s *TestSession) MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder {
|
||||
baseURL, err := url.Parse(setting.AppURL)
|
||||
assert.NoError(t, err)
|
||||
for _, c := range s.jar.Cookies(baseURL) {
|
||||
|
|
@ -167,7 +168,7 @@ func (s *TestSession) MakeRequest(t testing.TB, req *http.Request, expectedStatu
|
|||
resp := MakeRequest(t, req, expectedStatus)
|
||||
|
||||
ch := http.Header{}
|
||||
ch.Add("Cookie", strings.Join(resp.Headers["Set-Cookie"], ";"))
|
||||
ch.Add("Cookie", strings.Join(resp.HeaderMap["Set-Cookie"], ";"))
|
||||
cr := http.Request{Header: ch}
|
||||
s.jar.SetCookies(baseURL, cr.Cookies())
|
||||
|
||||
|
|
@ -207,7 +208,7 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
|
|||
resp = MakeRequest(t, req, http.StatusFound)
|
||||
|
||||
ch := http.Header{}
|
||||
ch.Add("Cookie", strings.Join(resp.Headers["Set-Cookie"], ";"))
|
||||
ch.Add("Cookie", strings.Join(resp.HeaderMap["Set-Cookie"], ";"))
|
||||
cr := http.Request{Header: ch}
|
||||
|
||||
session := emptyTestSession(t)
|
||||
|
|
@ -219,30 +220,6 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
|
|||
return session
|
||||
}
|
||||
|
||||
type TestResponseWriter struct {
|
||||
HeaderCode int
|
||||
Writer io.Writer
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) Header() http.Header {
|
||||
return w.Headers
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
func (w *TestResponseWriter) WriteHeader(n int) {
|
||||
w.HeaderCode = n
|
||||
}
|
||||
|
||||
type TestResponse struct {
|
||||
HeaderCode int
|
||||
Body []byte
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func NewRequest(t testing.TB, method, urlStr string) *http.Request {
|
||||
return NewRequestWithBody(t, method, urlStr, nil)
|
||||
}
|
||||
|
|
@ -278,26 +255,18 @@ func NewRequestWithBody(t testing.TB, method, urlStr string, body io.Reader) *ht
|
|||
|
||||
const NoExpectedStatus = -1
|
||||
|
||||
func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *TestResponse {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
respWriter := &TestResponseWriter{
|
||||
Writer: buffer,
|
||||
Headers: make(map[string][]string),
|
||||
}
|
||||
mac.ServeHTTP(respWriter, req)
|
||||
func MakeRequest(t testing.TB, req *http.Request, expectedStatus int) *httptest.ResponseRecorder {
|
||||
recorder := httptest.NewRecorder()
|
||||
mac.ServeHTTP(recorder, req)
|
||||
if expectedStatus != NoExpectedStatus {
|
||||
assert.EqualValues(t, expectedStatus, respWriter.HeaderCode,
|
||||
assert.EqualValues(t, expectedStatus, recorder.Code,
|
||||
"Request: %s %s", req.Method, req.URL.String())
|
||||
}
|
||||
return &TestResponse{
|
||||
HeaderCode: respWriter.HeaderCode,
|
||||
Body: buffer.Bytes(),
|
||||
Headers: respWriter.Headers,
|
||||
}
|
||||
return recorder
|
||||
}
|
||||
|
||||
func DecodeJSON(t testing.TB, resp *TestResponse, v interface{}) {
|
||||
decoder := json.NewDecoder(bytes.NewBuffer(resp.Body))
|
||||
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v interface{}) {
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
assert.NoError(t, decoder.Decode(v))
|
||||
}
|
||||
|
||||
|
|
@ -308,8 +277,8 @@ func GetCSRF(t testing.TB, session *TestSession, urlStr string) string {
|
|||
return doc.GetCSRF()
|
||||
}
|
||||
|
||||
func RedirectURL(t testing.TB, resp *TestResponse) string {
|
||||
urlSlice := resp.Headers["Location"]
|
||||
func RedirectURL(t testing.TB, resp *httptest.ResponseRecorder) string {
|
||||
urlSlice := resp.HeaderMap["Location"]
|
||||
assert.NotEmpty(t, urlSlice, "No redirect URL founds")
|
||||
return urlSlice[0]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ func assertProtectedBranch(t *testing.T, repoID int64, branchName string, isErr,
|
|||
|
||||
resp := MakeRequest(t, req, NoExpectedStatus)
|
||||
if isErr {
|
||||
assert.EqualValues(t, http.StatusInternalServerError, resp.HeaderCode)
|
||||
assert.EqualValues(t, http.StatusInternalServerError, resp.Code)
|
||||
} else {
|
||||
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
|
||||
assert.EqualValues(t, http.StatusOK, resp.Code)
|
||||
var branch models.ProtectedBranch
|
||||
t.Log(string(resp.Body))
|
||||
assert.NoError(t, json.Unmarshal(resp.Body, &branch))
|
||||
t.Log(resp.Body.String())
|
||||
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &branch))
|
||||
assert.Equal(t, canPush, !branch.IsProtected())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@ func TestPullCompare(t *testing.T) {
|
|||
|
||||
req = NewRequest(t, "GET", link)
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.EqualValues(t, http.StatusOK, resp.HeaderCode)
|
||||
assert.EqualValues(t, http.StatusOK, resp.Code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package integrations
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -13,7 +14,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testPullCreate(t *testing.T, session *TestSession, user, repo, branch string) *TestResponse {
|
||||
func testPullCreate(t *testing.T, session *TestSession, user, repo, branch string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", path.Join(user, repo))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package integrations
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -13,7 +14,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string) *TestResponse {
|
||||
func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ func testPullMerge(t *testing.T, session *TestSession, user, repo, pullnum strin
|
|||
return resp
|
||||
}
|
||||
|
||||
func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *TestResponse {
|
||||
func testPullCleanUp(t *testing.T, session *TestSession, user, repo, pullnum string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", path.Join(user, repo, "pulls", pullnum))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package integrations
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
|
|
@ -14,7 +15,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName string) *TestResponse {
|
||||
func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkOwnerName, forkRepoName string) *httptest.ResponseRecorder {
|
||||
forkOwner := models.AssertExistsAndLoadBean(t, &models.User{Name: forkOwnerName}).(*models.User)
|
||||
|
||||
// Step0: check the existence of the to-fork repo
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ package integrations
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *TestResponse {
|
||||
func testRepoMigrate(t testing.TB, session *TestSession, cloneAddr, repoName string) *httptest.ResponseRecorder {
|
||||
req := NewRequest(t, "GET", "/repo/migrate")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
|
|
|||
1
models/fixtures/reaction.yml
Normal file
1
models/fixtures/reaction.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
[] # empty
|
||||
|
|
@ -19,3 +19,11 @@ func valuesRepository(m map[int64]*Repository) []*Repository {
|
|||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func valuesUser(m map[int64]*User) []*User {
|
||||
var values = make([]*User, 0, len(m))
|
||||
for _, v := range m {
|
||||
values = append(values, v)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ type Issue struct {
|
|||
|
||||
Attachments []*Attachment `xorm:"-"`
|
||||
Comments []*Comment `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
}
|
||||
|
||||
// BeforeUpdate is invoked from XORM before updating this object.
|
||||
|
|
@ -155,6 +156,37 @@ func (issue *Issue) loadComments(e Engine) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
func (issue *Issue) loadReactions(e Engine) (err error) {
|
||||
if issue.Reactions != nil {
|
||||
return nil
|
||||
}
|
||||
reactions, err := findReactions(e, FindReactionsOptions{
|
||||
IssueID: issue.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load reaction user data
|
||||
if _, err := ReactionList(reactions).LoadUsers(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache comments to map
|
||||
comments := make(map[int64]*Comment)
|
||||
for _, comment := range issue.Comments {
|
||||
comments[comment.ID] = comment
|
||||
}
|
||||
// Add reactions either to issue or comment
|
||||
for _, react := range reactions {
|
||||
if react.CommentID == 0 {
|
||||
issue.Reactions = append(issue.Reactions, react)
|
||||
} else if comment, ok := comments[react.CommentID]; ok {
|
||||
comment.Reactions = append(comment.Reactions, react)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) loadAttributes(e Engine) (err error) {
|
||||
if err = issue.loadRepo(e); err != nil {
|
||||
return
|
||||
|
|
@ -192,10 +224,10 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
|
|||
}
|
||||
|
||||
if err = issue.loadComments(e); err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return issue.loadReactions(e)
|
||||
}
|
||||
|
||||
// LoadAttributes loads the attribute of this issue.
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ type Comment struct {
|
|||
CommitSHA string `xorm:"VARCHAR(40)"`
|
||||
|
||||
Attachments []*Attachment `xorm:"-"`
|
||||
Reactions ReactionList `xorm:"-"`
|
||||
|
||||
// For view issue page.
|
||||
ShowTag CommentTag `xorm:"-"`
|
||||
|
|
@ -305,6 +306,29 @@ func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Comment) loadReactions(e Engine) (err error) {
|
||||
if c.Reactions != nil {
|
||||
return nil
|
||||
}
|
||||
c.Reactions, err = findReactions(e, FindReactionsOptions{
|
||||
IssueID: c.IssueID,
|
||||
CommentID: c.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load reaction user data
|
||||
if _, err := c.Reactions.LoadUsers(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadReactions loads comment reactions
|
||||
func (c *Comment) LoadReactions() error {
|
||||
return c.loadReactions(x)
|
||||
}
|
||||
|
||||
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||
var LabelID int64
|
||||
if opts.Label != nil {
|
||||
|
|
|
|||
255
models/issue_reaction.go
Normal file
255
models/issue_reaction.go
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/builder"
|
||||
"github.com/go-xorm/xorm"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// Reaction represents a reactions on issues and comments.
|
||||
type Reaction struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
User *User `xorm:"-"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64 `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
func (s *Reaction) AfterLoad() {
|
||||
s.Created = time.Unix(s.CreatedUnix, 0).Local()
|
||||
}
|
||||
|
||||
// FindReactionsOptions describes the conditions to Find reactions
|
||||
type FindReactionsOptions struct {
|
||||
IssueID int64
|
||||
CommentID int64
|
||||
}
|
||||
|
||||
func (opts *FindReactionsOptions) toConds() builder.Cond {
|
||||
var cond = builder.NewCond()
|
||||
if opts.IssueID > 0 {
|
||||
cond = cond.And(builder.Eq{"reaction.issue_id": opts.IssueID})
|
||||
}
|
||||
if opts.CommentID > 0 {
|
||||
cond = cond.And(builder.Eq{"reaction.comment_id": opts.CommentID})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func findReactions(e Engine, opts FindReactionsOptions) ([]*Reaction, error) {
|
||||
reactions := make([]*Reaction, 0, 10)
|
||||
sess := e.Where(opts.toConds())
|
||||
return reactions, sess.
|
||||
Asc("reaction.issue_id", "reaction.comment_id", "reaction.created_unix", "reaction.id").
|
||||
Find(&reactions)
|
||||
}
|
||||
|
||||
func createReaction(e *xorm.Session, opts *ReactionOptions) (*Reaction, error) {
|
||||
reaction := &Reaction{
|
||||
Type: opts.Type,
|
||||
UserID: opts.Doer.ID,
|
||||
IssueID: opts.Issue.ID,
|
||||
}
|
||||
if opts.Comment != nil {
|
||||
reaction.CommentID = opts.Comment.ID
|
||||
}
|
||||
if _, err := e.Insert(reaction); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
// ReactionOptions defines options for creating or deleting reactions
|
||||
type ReactionOptions struct {
|
||||
Type string
|
||||
Doer *User
|
||||
Issue *Issue
|
||||
Comment *Comment
|
||||
}
|
||||
|
||||
// CreateReaction creates reaction for issue or comment.
|
||||
func CreateReaction(opts *ReactionOptions) (reaction *Reaction, err error) {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err = sess.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reaction, err = createReaction(sess, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = sess.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return reaction, nil
|
||||
}
|
||||
|
||||
// CreateIssueReaction creates a reaction on issue.
|
||||
func CreateIssueReaction(doer *User, issue *Issue, content string) (*Reaction, error) {
|
||||
return CreateReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
Doer: doer,
|
||||
Issue: issue,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateCommentReaction creates a reaction on comment.
|
||||
func CreateCommentReaction(doer *User, issue *Issue, comment *Comment, content string) (*Reaction, error) {
|
||||
return CreateReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
Doer: doer,
|
||||
Issue: issue,
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
|
||||
func deleteReaction(e *xorm.Session, opts *ReactionOptions) error {
|
||||
reaction := &Reaction{
|
||||
Type: opts.Type,
|
||||
UserID: opts.Doer.ID,
|
||||
IssueID: opts.Issue.ID,
|
||||
}
|
||||
if opts.Comment != nil {
|
||||
reaction.CommentID = opts.Comment.ID
|
||||
}
|
||||
_, err := e.Delete(reaction)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteReaction deletes reaction for issue or comment.
|
||||
func DeleteReaction(opts *ReactionOptions) error {
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteReaction(sess, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// DeleteIssueReaction deletes a reaction on issue.
|
||||
func DeleteIssueReaction(doer *User, issue *Issue, content string) error {
|
||||
return DeleteReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
Doer: doer,
|
||||
Issue: issue,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteCommentReaction deletes a reaction on comment.
|
||||
func DeleteCommentReaction(doer *User, issue *Issue, comment *Comment, content string) error {
|
||||
return DeleteReaction(&ReactionOptions{
|
||||
Type: content,
|
||||
Doer: doer,
|
||||
Issue: issue,
|
||||
Comment: comment,
|
||||
})
|
||||
}
|
||||
|
||||
// ReactionList represents list of reactions
|
||||
type ReactionList []*Reaction
|
||||
|
||||
// HasUser check if user has reacted
|
||||
func (list ReactionList) HasUser(userID int64) bool {
|
||||
if userID == 0 {
|
||||
return false
|
||||
}
|
||||
for _, reaction := range list {
|
||||
if reaction.UserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GroupByType returns reactions grouped by type
|
||||
func (list ReactionList) GroupByType() map[string]ReactionList {
|
||||
var reactions = make(map[string]ReactionList)
|
||||
for _, reaction := range list {
|
||||
reactions[reaction.Type] = append(reactions[reaction.Type], reaction)
|
||||
}
|
||||
return reactions
|
||||
}
|
||||
|
||||
func (list ReactionList) getUserIDs() []int64 {
|
||||
userIDs := make(map[int64]struct{}, len(list))
|
||||
for _, reaction := range list {
|
||||
if _, ok := userIDs[reaction.UserID]; !ok {
|
||||
userIDs[reaction.UserID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return keysInt64(userIDs)
|
||||
}
|
||||
|
||||
func (list ReactionList) loadUsers(e Engine) ([]*User, error) {
|
||||
if len(list) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
userIDs := list.getUserIDs()
|
||||
userMaps := make(map[int64]*User, len(userIDs))
|
||||
err := e.
|
||||
In("id", userIDs).
|
||||
Find(&userMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find user: %v", err)
|
||||
}
|
||||
|
||||
for _, reaction := range list {
|
||||
if user, ok := userMaps[reaction.UserID]; ok {
|
||||
reaction.User = user
|
||||
} else {
|
||||
reaction.User = NewGhostUser()
|
||||
}
|
||||
}
|
||||
return valuesUser(userMaps), nil
|
||||
}
|
||||
|
||||
// LoadUsers loads reactions' all users
|
||||
func (list ReactionList) LoadUsers() ([]*User, error) {
|
||||
return list.loadUsers(x)
|
||||
}
|
||||
|
||||
// GetFirstUsers returns first reacted user display names separated by comma
|
||||
func (list ReactionList) GetFirstUsers() string {
|
||||
var buffer bytes.Buffer
|
||||
var rem = setting.UI.ReactionMaxUserNum
|
||||
for _, reaction := range list {
|
||||
if buffer.Len() > 0 {
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
buffer.WriteString(reaction.User.DisplayName())
|
||||
if rem--; rem == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// GetMoreUserCount returns count of not shown users in reaction tooltip
|
||||
func (list ReactionList) GetMoreUserCount() int {
|
||||
if len(list) <= setting.UI.ReactionMaxUserNum {
|
||||
return 0
|
||||
}
|
||||
return len(list) - setting.UI.ReactionMaxUserNum
|
||||
}
|
||||
|
|
@ -149,7 +149,10 @@ var migrations = []Migration{
|
|||
// v49 -> v50
|
||||
NewMigration("add lfs lock table", addLFSLock),
|
||||
// v50 -> v51
|
||||
NewMigration("add reactions", addReactions),
|
||||
// v51 -> v52
|
||||
NewMigration("add issue_dependencies", addIssueDependencies),
|
||||
|
||||
}
|
||||
|
||||
// Migrate database to current version
|
||||
|
|
|
|||
|
|
@ -7,56 +7,22 @@ package migrations
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/go-xorm/xorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
func addIssueDependencies(x *xorm.Engine) (err error) {
|
||||
|
||||
type IssueDependency struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"NOT NULL"`
|
||||
IssueID int64 `xorm:"NOT NULL"`
|
||||
DependencyID int64 `xorm:"NOT NULL"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64 `xorm:"INDEX created"`
|
||||
Updated time.Time `xorm:"-"`
|
||||
UpdatedUnix int64 `xorm:"updated"`
|
||||
func addReactions(x *xorm.Engine) error {
|
||||
// Reaction see models/issue_reaction.go
|
||||
type Reaction struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
|
||||
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
|
||||
CreatedUnix int64 `xorm:"INDEX created"`
|
||||
}
|
||||
|
||||
if err = x.Sync(new(IssueDependency)); err != nil {
|
||||
return fmt.Errorf("Error creating issue_dependency_table column definition: %v", err)
|
||||
if err := x.Sync2(new(Reaction)); err != nil {
|
||||
return fmt.Errorf("Sync2: %v", err)
|
||||
}
|
||||
|
||||
// RepoUnit describes all units of a repository
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
Type int `xorm:"INDEX(s)"`
|
||||
Index int
|
||||
Config map[string]interface{} `xorm:"JSON"`
|
||||
CreatedUnix int64 `xorm:"INDEX CREATED"`
|
||||
Created time.Time `xorm:"-"`
|
||||
}
|
||||
|
||||
//Updating existing issue units
|
||||
units := make([]*RepoUnit, 0, 100)
|
||||
err = x.Where("`type` = ?", V16UnitTypeIssues).Find(&units)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Query repo units: %v", err)
|
||||
}
|
||||
for _, unit := range units {
|
||||
if unit.Config == nil {
|
||||
unit.Config = make(map[string]interface{})
|
||||
}
|
||||
if _, ok := unit.Config["EnableDependencies"]; !ok {
|
||||
unit.Config["EnableDependencies"] = setting.Service.DefaultEnableDependencies
|
||||
}
|
||||
if _, err := x.ID(unit.ID).Cols("config").Update(unit); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
1
models/migrations/v51.go
Normal file
1
models/migrations/v51.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
package migrations
|
||||
|
|
@ -119,6 +119,7 @@ func init() {
|
|||
new(RepoIndexerStatus),
|
||||
new(IssueDependency),
|
||||
new(LFSLock),
|
||||
new(Reaction),
|
||||
)
|
||||
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,18 @@ import (
|
|||
// RepositoryList contains a list of repositories
|
||||
type RepositoryList []*Repository
|
||||
|
||||
func (repos RepositoryList) Len() int {
|
||||
return len(repos)
|
||||
}
|
||||
|
||||
func (repos RepositoryList) Less(i, j int) bool {
|
||||
return repos[i].FullName() < repos[j].FullName()
|
||||
}
|
||||
|
||||
func (repos RepositoryList) Swap(i, j int) {
|
||||
repos[i], repos[j] = repos[j], repos[i]
|
||||
}
|
||||
|
||||
// RepositoryListOfMap make list from values of map
|
||||
func RepositoryListOfMap(repoMap map[int64]*Repository) RepositoryList {
|
||||
return RepositoryList(valuesRepository(repoMap))
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@ package models
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/go-xorm/xorm"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"code.gitea.io/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/sync"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/go-xorm/xorm"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// MirrorQueue holds an UniqueQueue object of the mirror
|
||||
|
|
@ -95,24 +95,6 @@ func (m *Mirror) readAddress() {
|
|||
}
|
||||
}
|
||||
|
||||
// HandleCloneUserCredentials replaces user credentials from HTTP/HTTPS URL
|
||||
// with placeholder <credentials>.
|
||||
// It will fail for any other forms of clone addresses.
|
||||
func HandleCloneUserCredentials(url string, mosaics bool) string {
|
||||
i := strings.Index(url, "@")
|
||||
if i == -1 {
|
||||
return url
|
||||
}
|
||||
start := strings.Index(url, "://")
|
||||
if start == -1 {
|
||||
return url
|
||||
}
|
||||
if mosaics {
|
||||
return url[:start+3] + "<credentials>" + url[i:]
|
||||
}
|
||||
return url[:start+3] + url[i+1:]
|
||||
}
|
||||
|
||||
// sanitizeOutput sanitizes output of a command, replacing occurrences of the
|
||||
// repository's remote address with a sanitized version.
|
||||
func sanitizeOutput(output, repoPath string) (string, error) {
|
||||
|
|
@ -122,14 +104,13 @@ func sanitizeOutput(output, repoPath string) (string, error) {
|
|||
// sanitize.
|
||||
return "", err
|
||||
}
|
||||
sanitized := HandleCloneUserCredentials(remoteAddr, true)
|
||||
return strings.Replace(output, remoteAddr, sanitized, -1), nil
|
||||
return util.SanitizeMessage(output, remoteAddr), nil
|
||||
}
|
||||
|
||||
// Address returns mirror address from Git repository config without credentials.
|
||||
func (m *Mirror) Address() string {
|
||||
m.readAddress()
|
||||
return HandleCloneUserCredentials(m.address, false)
|
||||
return util.SanitizeURLCredentials(m.address, false)
|
||||
}
|
||||
|
||||
// FullAddress returns mirror address from Git repository config.
|
||||
|
|
|
|||
|
|
@ -980,6 +980,7 @@ func deleteUser(e *xorm.Session, u *User) error {
|
|||
&IssueUser{UID: u.ID},
|
||||
&EmailAddress{UID: u.ID},
|
||||
&UserOpenID{UID: u.ID},
|
||||
&Reaction{UserID: u.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,6 +269,16 @@ func (f *CreateCommentForm) Validate(ctx *macaron.Context, errs binding.Errors)
|
|||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// ReactionForm form for adding and removing reaction
|
||||
type ReactionForm struct {
|
||||
Content string `binding:"Required;In(+1,-1,laugh,confused,heart,hooray)"`
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
func (f *ReactionForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
|
||||
return validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// _____ .__.__ __
|
||||
// / \ |__| | ____ _______/ |_ ____ ____ ____
|
||||
// / \ / \| | | _/ __ \ / ___/\ __\/ _ \ / \_/ __ \
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ func Contexter() macaron.Handler {
|
|||
ctx.Data["SignedUserName"] = ctx.User.Name
|
||||
ctx.Data["IsAdmin"] = ctx.User.IsAdmin
|
||||
} else {
|
||||
ctx.Data["SignedUserID"] = 0
|
||||
ctx.Data["SignedUserID"] = int64(0)
|
||||
ctx.Data["SignedUserName"] = ""
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ func RepoAssignment() macaron.Handler {
|
|||
return
|
||||
}
|
||||
ctx.Data["Branches"] = brs
|
||||
ctx.Data["BrancheCount"] = len(brs)
|
||||
ctx.Data["BranchesCount"] = len(brs)
|
||||
|
||||
// If not branch selected, try default one.
|
||||
// If default branch doesn't exists, fall back to some other branch.
|
||||
|
|
|
|||
|
|
@ -256,6 +256,7 @@ var (
|
|||
IssuePagingNum int
|
||||
RepoSearchPagingNum int
|
||||
FeedMaxCommitNum int
|
||||
ReactionMaxUserNum int
|
||||
ThemeColorMetaTag string
|
||||
MaxDisplayFileSize int64
|
||||
ShowUserEmail bool
|
||||
|
|
@ -279,6 +280,7 @@ var (
|
|||
IssuePagingNum: 10,
|
||||
RepoSearchPagingNum: 10,
|
||||
FeedMaxCommitNum: 5,
|
||||
ReactionMaxUserNum: 10,
|
||||
ThemeColorMetaTag: `#6cc644`,
|
||||
MaxDisplayFileSize: 8388608,
|
||||
Admin: struct {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"bytes"
|
||||
"container/list"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"mime"
|
||||
|
|
@ -162,6 +163,21 @@ func NewFuncMap() []template.FuncMap {
|
|||
return setting.DisableGitHooks
|
||||
},
|
||||
"TrN": TrN,
|
||||
"Dict": func(values ...interface{}) (map[string]interface{}, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("invalid dict call")
|
||||
}
|
||||
dict := make(map[string]interface{}, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("dict keys must be strings")
|
||||
}
|
||||
dict[key] = values[i+1]
|
||||
}
|
||||
return dict, nil
|
||||
},
|
||||
"Printf": fmt.Sprintf,
|
||||
}}
|
||||
}
|
||||
|
||||
|
|
|
|||
48
modules/util/sanitize.go
Normal file
48
modules/util/sanitize.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// urlSafeError wraps an error whose message may contain a sensitive URL
|
||||
type urlSafeError struct {
|
||||
err error
|
||||
unsanitizedURL string
|
||||
}
|
||||
|
||||
func (err urlSafeError) Error() string {
|
||||
return SanitizeMessage(err.err.Error(), err.unsanitizedURL)
|
||||
}
|
||||
|
||||
// URLSanitizedError returns the sanitized version an error whose message may
|
||||
// contain a sensitive URL
|
||||
func URLSanitizedError(err error, unsanitizedURL string) error {
|
||||
return urlSafeError{err: err, unsanitizedURL: unsanitizedURL}
|
||||
}
|
||||
|
||||
// SanitizeMessage sanitizes a message which may contains a sensitive URL
|
||||
func SanitizeMessage(message, unsanitizedURL string) string {
|
||||
sanitizedURL := SanitizeURLCredentials(unsanitizedURL, true)
|
||||
return strings.Replace(message, unsanitizedURL, sanitizedURL, -1)
|
||||
}
|
||||
|
||||
// SanitizeURLCredentials sanitizes a url, either removing user credentials
|
||||
// or replacing them with a placeholder.
|
||||
func SanitizeURLCredentials(unsanitizedURL string, usePlaceholder bool) string {
|
||||
u, err := url.Parse(unsanitizedURL)
|
||||
if err != nil {
|
||||
// don't log the error, since it might contain unsanitized URL.
|
||||
return "(unparsable url)"
|
||||
}
|
||||
if u.User != nil && usePlaceholder {
|
||||
u.User = url.User("<credentials>")
|
||||
} else {
|
||||
u.User = nil
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
|
@ -489,6 +489,8 @@ mirror_last_synced = Last Synced
|
|||
watchers = Watchers
|
||||
stargazers = Stargazers
|
||||
forks = Forks
|
||||
pick_reaction = Pick your reaction
|
||||
reactions_more = and %d more
|
||||
|
||||
form.reach_limit_of_creation = You have already reached your limit of %d repositories.
|
||||
form.name_reserved = The repository name '%s' is reserved.
|
||||
|
|
@ -539,6 +541,7 @@ pulls = Pull Requests
|
|||
labels = Labels
|
||||
milestones = Milestones
|
||||
commits = Commits
|
||||
commit = Commit
|
||||
releases = Releases
|
||||
file_raw = Raw
|
||||
file_history = History
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -117,6 +117,54 @@ function updateIssuesMeta(url, action, issueIds, elementId, afterSuccess) {
|
|||
})
|
||||
}
|
||||
|
||||
function initReactionSelector(parent) {
|
||||
var reactions = '';
|
||||
if (!parent) {
|
||||
parent = $(document);
|
||||
reactions = '.reactions > ';
|
||||
}
|
||||
|
||||
parent.find(reactions + 'a.label').popup({'position': 'bottom left', 'metadata': {'content': 'title', 'title': 'none'}});
|
||||
|
||||
parent.find('.select-reaction > .menu > .item, ' + reactions + 'a.label').on('click', function(e){
|
||||
var vm = this;
|
||||
e.preventDefault();
|
||||
|
||||
if ($(this).hasClass('disabled')) return;
|
||||
|
||||
var actionURL = $(this).hasClass('item') ?
|
||||
$(this).closest('.select-reaction').data('action-url') :
|
||||
$(this).data('action-url');
|
||||
var url = actionURL + '/' + ($(this).hasClass('blue') ? 'unreact' : 'react');
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: {
|
||||
'_csrf': csrf,
|
||||
'content': $(this).data('content')
|
||||
}
|
||||
}).done(function(resp) {
|
||||
if (resp && (resp.html || resp.empty)) {
|
||||
var content = $(vm).closest('.content');
|
||||
var react = content.find('.segment.reactions');
|
||||
if (react.length > 0) {
|
||||
react.remove();
|
||||
}
|
||||
if (!resp.empty) {
|
||||
react = $('<div class="ui attached segment reactions"></div>').appendTo(content);
|
||||
react.html(resp.html);
|
||||
var hasEmoji = react.find('.has-emoji');
|
||||
for (var i = 0; i < hasEmoji.length; i++) {
|
||||
emojify.run(hasEmoji.get(i));
|
||||
}
|
||||
react.find('.dropdown').dropdown();
|
||||
initReactionSelector(react);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initCommentForm() {
|
||||
if ($('.comment.form').length == 0) {
|
||||
return
|
||||
|
|
@ -594,6 +642,7 @@ function initRepository() {
|
|||
$('#status').val($statusButton.data('status-val'));
|
||||
$('#comment-form').submit();
|
||||
});
|
||||
initReactionSelector();
|
||||
}
|
||||
|
||||
// Diff
|
||||
|
|
|
|||
|
|
@ -548,7 +548,7 @@
|
|||
}
|
||||
.content {
|
||||
margin-left: 4em;
|
||||
.header {
|
||||
> .header {
|
||||
#avatar-arrow;
|
||||
font-weight: normal;
|
||||
padding: auto 15px;
|
||||
|
|
@ -1350,6 +1350,43 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.segment.reactions, .select-reaction {
|
||||
&.dropdown .menu {
|
||||
right: 0!important;
|
||||
left: auto!important;
|
||||
> .header {
|
||||
margin: 0.75rem 0 .5rem;
|
||||
}
|
||||
> .item {
|
||||
float: left;
|
||||
padding: .5rem .5rem !important;
|
||||
img.emoji {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.segment.reactions {
|
||||
padding: .3em 1em;
|
||||
.ui.label {
|
||||
padding: .4em;
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
> img {
|
||||
height: 1.5em !important;
|
||||
}
|
||||
}
|
||||
.select-reaction {
|
||||
float: none;
|
||||
&:not(.active) a {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&:hover .select-reaction a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
// End of .repository
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
|
|
@ -18,6 +16,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/v1/convert"
|
||||
api "code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// Search repositories via options
|
||||
|
|
@ -327,12 +326,13 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) {
|
|||
RemoteAddr: remoteAddr,
|
||||
})
|
||||
if err != nil {
|
||||
err = util.URLSanitizedError(err, remoteAddr)
|
||||
if repo != nil {
|
||||
if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
|
||||
log.Error(4, "DeleteRepository: %v", errDelete)
|
||||
}
|
||||
}
|
||||
ctx.Error(500, "MigrateRepository", models.HandleCloneUserCredentials(err.Error(), true))
|
||||
ctx.Error(500, "MigrateRepository", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ const (
|
|||
tplMilestoneNew base.TplName = "repo/issue/milestone_new"
|
||||
tplMilestoneEdit base.TplName = "repo/issue/milestone_edit"
|
||||
|
||||
tplReactions base.TplName = "repo/issue/view_content/reactions"
|
||||
|
||||
issueTemplateKey = "IssueTemplate"
|
||||
)
|
||||
|
||||
|
|
@ -739,9 +741,8 @@ func GetActionIssue(ctx *context.Context) *models.Issue {
|
|||
ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
|
||||
return nil
|
||||
}
|
||||
if issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) ||
|
||||
!issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypeIssues) {
|
||||
ctx.Handle(404, "IssueOrPullRequestUnitNotAllowed", nil)
|
||||
checkIssueRights(ctx, issue)
|
||||
if ctx.Written() {
|
||||
return nil
|
||||
}
|
||||
if err = issue.LoadAttributes(); err != nil {
|
||||
|
|
@ -751,6 +752,13 @@ func GetActionIssue(ctx *context.Context) *models.Issue {
|
|||
return issue
|
||||
}
|
||||
|
||||
func checkIssueRights(ctx *context.Context, issue *models.Issue) {
|
||||
if issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypePullRequests) ||
|
||||
!issue.IsPull && !ctx.Repo.Repository.UnitEnabled(models.UnitTypeIssues) {
|
||||
ctx.Handle(404, "IssueOrPullRequestUnitNotAllowed", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func getActionIssues(ctx *context.Context) []*models.Issue {
|
||||
commaSeparatedIssueIDs := ctx.Query("issue_ids")
|
||||
if len(commaSeparatedIssueIDs) == 0 {
|
||||
|
|
@ -1289,3 +1297,146 @@ func DeleteMilestone(ctx *context.Context) {
|
|||
"redirect": ctx.Repo.RepoLink + "/milestones",
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeIssueReaction create a reaction for issue
|
||||
func ChangeIssueReaction(ctx *context.Context, form auth.ReactionForm) {
|
||||
issue := GetActionIssue(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.Handle(500, "ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
|
||||
return
|
||||
}
|
||||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
|
||||
if err != nil {
|
||||
log.Info("CreateIssueReaction: %s", err)
|
||||
break
|
||||
}
|
||||
// Reload new reactions
|
||||
issue.Reactions = nil
|
||||
if err = issue.LoadAttributes(); err != nil {
|
||||
log.Info("issue.LoadAttributes: %s", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
|
||||
case "unreact":
|
||||
if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
|
||||
ctx.Handle(500, "DeleteIssueReaction", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Reload new reactions
|
||||
issue.Reactions = nil
|
||||
if err := issue.LoadAttributes(); err != nil {
|
||||
log.Info("issue.LoadAttributes: %s", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
|
||||
default:
|
||||
ctx.Handle(404, fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(issue.Reactions) == 0 {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"empty": true,
|
||||
"html": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
|
||||
"ctx": ctx.Data,
|
||||
"ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
|
||||
"Reactions": issue.Reactions.GroupByType(),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Handle(500, "ChangeIssueReaction.HTMLString", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"html": html,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangeCommentReaction create a reaction for comment
|
||||
func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) {
|
||||
comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
|
||||
return
|
||||
}
|
||||
|
||||
issue, err := models.GetIssueByID(comment.IssueID)
|
||||
checkIssueRights(ctx, issue)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.Handle(500, "ChangeCommentReaction", errors.New(ctx.GetErrMsg()))
|
||||
return
|
||||
}
|
||||
|
||||
switch ctx.Params(":action") {
|
||||
case "react":
|
||||
reaction, err := models.CreateCommentReaction(ctx.User, issue, comment, form.Content)
|
||||
if err != nil {
|
||||
log.Info("CreateCommentReaction: %s", err)
|
||||
break
|
||||
}
|
||||
// Reload new reactions
|
||||
comment.Reactions = nil
|
||||
if err = comment.LoadReactions(); err != nil {
|
||||
log.Info("comment.LoadReactions: %s", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID, reaction.ID)
|
||||
case "unreact":
|
||||
if err := models.DeleteCommentReaction(ctx.User, issue, comment, form.Content); err != nil {
|
||||
ctx.Handle(500, "DeleteCommentReaction", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Reload new reactions
|
||||
comment.Reactions = nil
|
||||
if err = comment.LoadReactions(); err != nil {
|
||||
log.Info("comment.LoadReactions: %s", err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
|
||||
default:
|
||||
ctx.Handle(404, fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if len(comment.Reactions) == 0 {
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"empty": true,
|
||||
"html": "",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
|
||||
"ctx": ctx.Data,
|
||||
"ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
|
||||
"Reactions": comment.Reactions.GroupByType(),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Handle(500, "ChangeCommentReaction.HTMLString", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(200, map[string]interface{}{
|
||||
"html": html,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -232,6 +233,9 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) {
|
|||
return
|
||||
}
|
||||
|
||||
// remoteAddr may contain credentials, so we sanitize it
|
||||
err = util.URLSanitizedError(err, remoteAddr)
|
||||
|
||||
if repo != nil {
|
||||
if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil {
|
||||
log.Error(4, "DeleteRepository: %v", errDelete)
|
||||
|
|
@ -241,11 +245,11 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) {
|
|||
if strings.Contains(err.Error(), "Authentication failed") ||
|
||||
strings.Contains(err.Error(), "could not read Username") {
|
||||
ctx.Data["Err_Auth"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("form.auth_failed", models.HandleCloneUserCredentials(err.Error(), true)), tplMigrate, &form)
|
||||
ctx.RenderWithErr(ctx.Tr("form.auth_failed", err.Error()), tplMigrate, &form)
|
||||
return
|
||||
} else if strings.Contains(err.Error(), "fatal:") {
|
||||
ctx.Data["Err_CloneAddr"] = true
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", models.HandleCloneUserCredentials(err.Error(), true)), tplMigrate, &form)
|
||||
ctx.RenderWithErr(ctx.Tr("repo.migrate.failed", err.Error()), tplMigrate, &form)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -499,6 +499,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
m.Post("/cancel", repo.CancelStopwatch)
|
||||
})
|
||||
})
|
||||
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeIssueReaction)
|
||||
})
|
||||
|
||||
m.Post("/labels", reqRepoWriter, repo.UpdateIssueLabel)
|
||||
|
|
@ -509,6 +510,7 @@ func RegisterRoutes(m *macaron.Macaron) {
|
|||
m.Group("/comments/:id", func() {
|
||||
m.Post("", repo.UpdateCommentContent)
|
||||
m.Post("/delete", repo.DeleteComment)
|
||||
m.Post("/reactions/:action", bindIgnErr(auth.ReactionForm{}), repo.ChangeCommentReaction)
|
||||
}, context.CheckAnyUnit(models.UnitTypeIssues, models.UnitTypePullRequests))
|
||||
m.Group("/labels", func() {
|
||||
m.Post("/new", bindIgnErr(auth.CreateLabelForm{}), repo.NewLabel)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ package user
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/Unknwon/com"
|
||||
"github.com/Unknwon/paginater"
|
||||
|
|
@ -302,6 +303,7 @@ func Issues(ctx *context.Context) {
|
|||
}
|
||||
|
||||
showRepos := models.RepositoryListOfMap(showReposMap)
|
||||
sort.Sort(showRepos)
|
||||
if err = showRepos.LoadAttributes(); err != nil {
|
||||
ctx.Handle(500, "LoadAttributes", fmt.Errorf("%v", err))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
<div class="ui top attached header">
|
||||
<span class="text grey"><a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.Name}}</a> {{.i18n.Tr "repo.issues.commented_at" .Issue.HashTag $createdStr | Safe}}</span>
|
||||
<div class="ui right actions">
|
||||
{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) }}
|
||||
{{if .IsIssueOwner}}
|
||||
<div class="item action">
|
||||
<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
|
||||
|
|
@ -37,6 +38,12 @@
|
|||
<div class="raw-content hide">{{.Issue.Content}}</div>
|
||||
<div class="edit-content-zone hide" data-write="issue-{{.Issue.ID}}-write" data-preview="issue-{{.Issue.ID}}-preview" data-update-url="{{$.RepoLink}}/issues/{{.Issue.Index}}/content" data-context="{{.RepoLink}}"></div>
|
||||
</div>
|
||||
{{$reactions := .Issue.Reactions.GroupByType}}
|
||||
{{if $reactions}}
|
||||
<div class="ui attached segment reactions">
|
||||
{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/issues/%d/reactions" $.RepoLink .Issue.Index) "Reactions" $reactions }}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Issue.Attachments}}
|
||||
<div class="ui bottom attached segment">
|
||||
<div class="ui small images">
|
||||
|
|
|
|||
18
templates/repo/issue/view_content/add_reaction.tmpl
Normal file
18
templates/repo/issue/view_content/add_reaction.tmpl
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{{if .ctx.IsSigned}}
|
||||
<div class="item action ui pointing top right select-reaction dropdown" data-action-url="{{ .ActionURL }}">
|
||||
<a class="add-reaction">
|
||||
<i class="octicon octicon-plus-small" style="width: 10px"></i>
|
||||
<i class="octicon octicon-smiley"></i>
|
||||
</a>
|
||||
<div class="menu has-emoji">
|
||||
<div class="header">{{ .ctx.i18n.Tr "repo.pick_reaction"}}</div>
|
||||
<div class="divider"></div>
|
||||
<div class="item" data-content="+1">:+1:</div>
|
||||
<div class="item" data-content="-1">:-1:</div>
|
||||
<div class="item" data-content="laugh">:laughing:</div>
|
||||
<div class="item" data-content="confused">:confused:</div>
|
||||
<div class="item" data-content="heart">:heart:</div>
|
||||
<div class="item" data-content="hooray">:tada:</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "repo/issue/view_content/add_reaction" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) }}
|
||||
{{if or $.IsRepositoryAdmin (eq .Poster.ID $.SignedUserID)}}
|
||||
<div class="item action">
|
||||
<a class="edit-content" href="#"><i class="octicon octicon-pencil"></i></a>
|
||||
|
|
@ -41,6 +42,12 @@
|
|||
<div class="raw-content hide">{{.Content}}</div>
|
||||
<div class="edit-content-zone hide" data-write="issuecomment-{{.ID}}-write" data-preview="issuecomment-{{.ID}}-preview" data-update-url="{{$.RepoLink}}/comments/{{.ID}}" data-context="{{$.RepoLink}}"></div>
|
||||
</div>
|
||||
{{$reactions := .Reactions.GroupByType}}
|
||||
{{if $reactions}}
|
||||
<div class="ui attached segment reactions">
|
||||
{{template "repo/issue/view_content/reactions" Dict "ctx" $ "ActionURL" (Printf "%s/comments/%d/reactions" $.RepoLink .ID) "Reactions" $reactions }}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Attachments}}
|
||||
<div class="ui bottom attached segment">
|
||||
<div class="ui small images">
|
||||
|
|
|
|||
15
templates/repo/issue/view_content/reactions.tmpl
Normal file
15
templates/repo/issue/view_content/reactions.tmpl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{{range $key, $value := .Reactions}}
|
||||
<a class="ui label basic{{if $value.HasUser $.ctx.SignedUserID}} blue{{end}}{{if not $.ctx.IsSigned}} disabled{{end}} has-emoji" data-title="{{$value.GetFirstUsers}}{{if gt ($value.GetMoreUserCount) 0}} {{ $.ctx.i18n.Tr "repo.reactions_more" $value.GetMoreUserCount}}{{end}}" data-content="{{ $key }}" data-action-url="{{ $.ActionURL }}">
|
||||
{{if eq $key "hooray"}}
|
||||
:tada:
|
||||
{{else}}
|
||||
{{if eq $key "laugh"}}
|
||||
:laughing:
|
||||
{{else}}
|
||||
:{{$key}}:
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{len $value}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{template "repo/issue/view_content/add_reaction" Dict "ctx" $.ctx "ActionURL" .ActionURL }}
|
||||
|
|
@ -2,12 +2,12 @@
|
|||
<div class="ui two horizontal center link list">
|
||||
{{if and (.Repository.UnitEnabled $.UnitTypeCode) (not .IsBareRepo)}}
|
||||
<div class="item{{if .PageIsCommits}} active{{end}}">
|
||||
<a href="{{.RepoLink}}/commits{{if .IsViewBranch}}/branch{{else if .IsViewTag}}/tag{{else if .IsViewCommit}}/commit{{end}}/{{EscapePound .BranchName}}"><i class="octicon octicon-history"></i> <b>{{.CommitsCount}}</b> {{.i18n.Tr "repo.commits"}}</a>
|
||||
<a href="{{.RepoLink}}/commits{{if .IsViewBranch}}/branch{{else if .IsViewTag}}/tag{{else if .IsViewCommit}}/commit{{end}}/{{EscapePound .BranchName}}"><i class="octicon octicon-history"></i> <b>{{.CommitsCount}}</b> {{.i18n.Tr (TrN .i18n.Lang .CommitsCount "repo.commit" "repo.commits") }}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and (.Repository.UnitEnabled $.UnitTypeCode) (not .IsBareRepo) }}
|
||||
<div class="item{{if .PageIsBranches}} active{{end}}">
|
||||
<a href="{{.RepoLink}}/branches/"><i class="octicon octicon-git-branch"></i> <b>{{.BrancheCount}}</b> {{.i18n.Tr "repo.branches"}}</a>
|
||||
<a href="{{.RepoLink}}/branches/"><i class="octicon octicon-git-branch"></i> <b>{{.BranchesCount}}</b> {{.i18n.Tr (TrN .i18n.Lang .BranchesCount "repo.branch" "repo.branches") }}</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user