Browse Source

add webhook recent deliveries

Unknwon 9 years ago
parent
commit
23f42d92c9

+ 2 - 0
conf/app.ini

@@ -97,6 +97,8 @@ QUEUE_LENGTH = 1000
 DELIVER_TIMEOUT = 5
 ; Allow insecure certification
 SKIP_TLS_VERIFY = false
+; Number of history information in each page
+PAGING_NUM = 10
 
 [mailer]
 ENABLED = false

+ 5 - 0
conf/locale/locale_en-US.ini

@@ -513,6 +513,11 @@ settings.hooks_desc = Webhooks are much like basic HTTP POST event triggers. Whe
 settings.webhook_deletion = Delete Webhook
 settings.webhook_deletion_desc = Delete this webhook will remove its information and all delivery history. Do you want to continue?
 settings.webhook_deletion_success = Webhook has been deleted successfully!
+settings.webhook.request = Request
+settings.webhook.response = Response
+settings.webhook.headers = Headers
+settings.webhook.payload = Payload
+settings.webhook.body = Body
 settings.githooks_desc = Git Hooks are powered by Git itself, you can edit files of supported hooks in the list below to perform custom operations.
 settings.githook_edit_desc = If the hook is inactive, sample content will be presented. Leaving content to an empty value will disable this hook.
 settings.githook_name = Hook Name

+ 2 - 2
models/action.go

@@ -448,11 +448,11 @@ func CommitRepoAction(userID, repoUserID int64, userName, actEmail string,
 			RepoID:      repo.ID,
 			HookID:      w.ID,
 			Type:        w.HookTaskType,
-			Url:         w.URL,
+			URL:         w.URL,
 			BasePayload: payload,
 			ContentType: w.ContentType,
 			EventType:   HOOK_EVENT_PUSH,
-			IsSsl:       w.IsSSL,
+			IsSSL:       w.IsSSL,
 		}); err != nil {
 			return fmt.Errorf("CreateHookTask: %v", err)
 		}

+ 21 - 1
models/error.go

@@ -236,7 +236,27 @@ func IsErrRepoAlreadyExist(err error) bool {
 }
 
 func (err ErrRepoAlreadyExist) Error() string {
-	return fmt.Sprintf("repository already exists [uname: %d, name: %s]", err.Uname, err.Name)
+	return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
+}
+
+//  __      __      ___.   .__                   __
+// /  \    /  \ ____\_ |__ |  |__   ____   ____ |  | __
+// \   \/\/   // __ \| __ \|  |  \ /  _ \ /  _ \|  |/ /
+//  \        /\  ___/| \_\ \   Y  (  <_> |  <_> )    <
+//   \__/\  /  \___  >___  /___|  /\____/ \____/|__|_ \
+//        \/       \/    \/     \/                   \/
+
+type ErrWebhookNotExist struct {
+	ID int64
+}
+
+func IsErrWebhookNotExist(err error) bool {
+	_, ok := err.(ErrWebhookNotExist)
+	return ok
+}
+
+func (err ErrWebhookNotExist) Error() string {
+	return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
 }
 
 // .___

+ 5 - 0
models/models.go

@@ -84,6 +84,11 @@ func init() {
 		new(UpdateTask), new(HookTask),
 		new(Team), new(OrgUser), new(TeamUser), new(TeamRepo),
 		new(Notice), new(EmailAddress))
+
+	gonicNames := []string{"SSL"}
+	for _, name := range gonicNames {
+		core.LintGonicMapper[name] = true
+	}
 }
 
 func LoadModelsConfig() {

+ 161 - 59
models/webhook.go

@@ -7,21 +7,20 @@ package models
 import (
 	"crypto/tls"
 	"encoding/json"
-	"errors"
+	"fmt"
 	"io/ioutil"
+	"strings"
 	"sync"
 	"time"
 
+	"github.com/go-xorm/xorm"
+
 	"github.com/gogits/gogs/modules/httplib"
 	"github.com/gogits/gogs/modules/log"
 	"github.com/gogits/gogs/modules/setting"
 	"github.com/gogits/gogs/modules/uuid"
 )
 
-var (
-	ErrWebhookNotExist = errors.New("Webhook does not exist")
-)
-
 type HookContentType int
 
 const (
@@ -103,6 +102,11 @@ func (w *Webhook) GetSlackHook() *Slack {
 	return s
 }
 
+// History returns history of webhook by given conditions.
+func (w *Webhook) History(page int) ([]*HookTask, error) {
+	return HookTasks(w.ID, page)
+}
+
 // UpdateEvent handles conversion from HookEvent to Events.
 func (w *Webhook) UpdateEvent() error {
 	data, err := json.Marshal(w.HookEvent)
@@ -124,14 +128,14 @@ func CreateWebhook(w *Webhook) error {
 	return err
 }
 
-// GetWebhookById returns webhook by given ID.
-func GetWebhookById(hookId int64) (*Webhook, error) {
-	w := &Webhook{ID: hookId}
-	has, err := x.Get(w)
+// GetWebhookByID returns webhook by given ID.
+func GetWebhookByID(id int64) (*Webhook, error) {
+	w := new(Webhook)
+	has, err := x.Id(id).Get(w)
 	if err != nil {
 		return nil, err
 	} else if !has {
-		return nil, ErrWebhookNotExist
+		return nil, ErrWebhookNotExist{id}
 	}
 	return w, nil
 }
@@ -271,29 +275,99 @@ type Payload struct {
 }
 
 func (p Payload) GetJSONPayload() ([]byte, error) {
-	data, err := json.Marshal(p)
+	data, err := json.MarshalIndent(p, "", "  ")
 	if err != nil {
 		return []byte{}, err
 	}
 	return data, nil
 }
 
+// HookRequest represents hook task request information.
+type HookRequest struct {
+	Headers map[string]string `json:"headers"`
+}
+
+// HookResponse represents hook task response information.
+type HookResponse struct {
+	Status  int               `json:"status"`
+	Headers map[string]string `json:"headers"`
+	Body    string            `json:"body"`
+}
+
 // HookTask represents a hook task.
 type HookTask struct {
-	ID             int64 `xorm:"pk autoincr"`
-	RepoID         int64 `xorm:"INDEX"`
-	HookID         int64
-	Uuid           string
-	Type           HookTaskType
-	Url            string
-	BasePayload    `xorm:"-"`
-	PayloadContent string `xorm:"TEXT"`
-	ContentType    HookContentType
-	EventType      HookEventType
-	IsSsl          bool
-	IsDelivered    bool
-	Delivered      int64
-	IsSucceed      bool
+	ID              int64 `xorm:"pk autoincr"`
+	RepoID          int64 `xorm:"INDEX"`
+	HookID          int64
+	UUID            string
+	Type            HookTaskType
+	URL             string
+	BasePayload     `xorm:"-"`
+	PayloadContent  string `xorm:"TEXT"`
+	ContentType     HookContentType
+	EventType       HookEventType
+	IsSSL           bool
+	IsDelivered     bool
+	Delivered       int64
+	DeliveredString string `xorm:"-"`
+
+	// History info.
+	IsSucceed       bool
+	RequestContent  string        `xorm:"TEXT"`
+	RequestInfo     *HookRequest  `xorm:"-"`
+	ResponseContent string        `xorm:"TEXT"`
+	ResponseInfo    *HookResponse `xorm:"-"`
+}
+
+func (t *HookTask) BeforeUpdate() {
+	if t.RequestInfo != nil {
+		t.RequestContent = t.MarshalJSON(t.RequestInfo)
+	}
+	if t.ResponseInfo != nil {
+		t.ResponseContent = t.MarshalJSON(t.ResponseInfo)
+	}
+}
+
+func (t *HookTask) AfterSet(colName string, _ xorm.Cell) {
+	var err error
+	switch colName {
+	case "delivered":
+		t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST")
+
+	case "request_content":
+		if len(t.RequestContent) == 0 {
+			return
+		}
+
+		t.RequestInfo = &HookRequest{}
+		if err = json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
+			log.Error(3, "Unmarshal[%d]: %v", t.ID, err)
+		}
+
+	case "response_content":
+		if len(t.ResponseContent) == 0 {
+			return
+		}
+
+		t.ResponseInfo = &HookResponse{}
+		if err = json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
+			log.Error(3, "Unmarshal[%d]: %v", t.ID, err)
+		}
+	}
+}
+
+func (t *HookTask) MarshalJSON(v interface{}) string {
+	p, err := json.Marshal(v)
+	if err != nil {
+		log.Error(3, "Marshal[%d]: %v", t.ID, err)
+	}
+	return string(p)
+}
+
+// HookTasks returns a list of hook tasks by given conditions.
+func HookTasks(hookID int64, page int) ([]*HookTask, error) {
+	tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
+	return tasks, x.Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).Desc("id").Find(&tasks)
 }
 
 // CreateHookTask creates a new hook task,
@@ -303,7 +377,7 @@ func CreateHookTask(t *HookTask) error {
 	if err != nil {
 		return err
 	}
-	t.Uuid = uuid.NewV4().String()
+	t.UUID = uuid.NewV4().String()
 	t.PayloadContent = string(data)
 	_, err = x.Insert(t)
 	return err
@@ -348,9 +422,11 @@ func (q *hookQueue) AddRepoID(id int64) {
 var HookQueue *hookQueue
 
 func deliverHook(t *HookTask) {
+	t.IsDelivered = true
+
 	timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second
-	req := httplib.Post(t.Url).SetTimeout(timeout, timeout).
-		Header("X-Gogs-Delivery", t.Uuid).
+	req := httplib.Post(t.URL).SetTimeout(timeout, timeout).
+		Header("X-Gogs-Delivery", t.UUID).
 		Header("X-Gogs-Event", string(t.EventType)).
 		SetTLSClientConfig(&tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify})
 
@@ -361,42 +437,68 @@ func deliverHook(t *HookTask) {
 		req.Param("payload", t.PayloadContent)
 	}
 
-	t.IsDelivered = true
+	// Record delivery information.
+	t.RequestInfo = &HookRequest{
+		Headers: map[string]string{},
+	}
+	for k, vals := range req.Headers() {
+		t.RequestInfo.Headers[k] = strings.Join(vals, ",")
+	}
 
-	// FIXME: record response.
-	switch t.Type {
-	case GOGS:
-		{
-			if resp, err := req.Response(); err != nil {
-				log.Error(5, "Delivery: %v", err)
-			} else {
-				resp.Body.Close()
-				t.IsSucceed = true
-			}
+	t.ResponseInfo = &HookResponse{
+		Headers: map[string]string{},
+	}
+
+	defer func() {
+		t.Delivered = time.Now().UTC().UnixNano()
+		if t.IsSucceed {
+			log.Trace("Hook delivered: %s", t.UUID)
 		}
-	case SLACK:
-		{
-			if resp, err := req.Response(); err != nil {
-				log.Error(5, "Delivery: %v", err)
-			} else {
-				defer resp.Body.Close()
-				contents, err := ioutil.ReadAll(resp.Body)
-				if err != nil {
-					log.Error(5, "%s", err)
-				} else {
-					if string(contents) != "ok" {
-						log.Error(5, "slack failed with: %s", string(contents))
-					} else {
-						t.IsSucceed = true
-					}
-				}
-			}
+
+		// Update webhook last delivery status.
+		w, err := GetWebhookByID(t.HookID)
+		if err != nil {
+			log.Error(5, "GetWebhookByID: %v", err)
+			return
+		}
+		if t.IsSucceed {
+			w.LastStatus = HOOK_STATUS_SUCCEED
+		} else {
+			w.LastStatus = HOOK_STATUS_FAILED
 		}
+		if err = UpdateWebhook(w); err != nil {
+			log.Error(5, "UpdateWebhook: %v", err)
+			return
+		}
+	}()
+
+	resp, err := req.Response()
+	if err != nil {
+		t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err)
+		return
 	}
+	defer resp.Body.Close()
 
-	t.Delivered = time.Now().UTC().UnixNano()
-	if t.IsSucceed {
-		log.Trace("Hook delivered(%s): %s", t.Uuid, t.PayloadContent)
+	// Status code is 20x can be seen as succeed.
+	t.IsSucceed = resp.StatusCode/100 == 2
+	t.ResponseInfo.Status = resp.StatusCode
+	for k, vals := range resp.Header {
+		t.ResponseInfo.Headers[k] = strings.Join(vals, ",")
+	}
+
+	p, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err)
+		return
+	}
+	t.ResponseInfo.Body = string(p)
+
+	switch t.Type {
+	case SLACK:
+		if t.ResponseInfo.Body != "ok" {
+			log.Error(5, "slack failed with: %s", t.ResponseInfo.Body)
+			t.IsSucceed = false
+		}
 	}
 }
 

File diff suppressed because it is too large
+ 0 - 0
modules/bindata/bindata.go


+ 123 - 122
modules/httplib/httplib.go

@@ -5,8 +5,6 @@
 
 package httplib
 
-// NOTE: last sync 57e62e5 on Oct 29, 2014.
-
 import (
 	"bytes"
 	"crypto/tls"
@@ -27,7 +25,7 @@ import (
 	"time"
 )
 
-var defaultSetting = BeegoHttpSettings{false, "beegoServer", 60 * time.Second, 60 * time.Second, nil, nil, nil, false}
+var defaultSetting = Settings{false, "GogsServer", 60 * time.Second, 60 * time.Second, nil, nil, nil, false}
 var defaultCookieJar http.CookieJar
 var settingMutex sync.Mutex
 
@@ -39,7 +37,7 @@ func createDefaultCookie() {
 }
 
 // Overwrite default settings
-func SetDefaultSetting(setting BeegoHttpSettings) {
+func SetDefaultSetting(setting Settings) {
 	settingMutex.Lock()
 	defer settingMutex.Unlock()
 	defaultSetting = setting
@@ -51,8 +49,8 @@ func SetDefaultSetting(setting BeegoHttpSettings) {
 	}
 }
 
-// return *BeegoHttpRequest with specific method
-func newBeegoRequest(url, method string) *BeegoHttpRequest {
+// return *Request with specific method
+func newBeegoRequest(url, method string) *Request {
 	var resp http.Response
 	req := http.Request{
 		Method:     method,
@@ -61,36 +59,35 @@ func newBeegoRequest(url, method string) *BeegoHttpRequest {
 		ProtoMajor: 1,
 		ProtoMinor: 1,
 	}
-	return &BeegoHttpRequest{url, &req, map[string]string{}, map[string]string{}, defaultSetting, &resp, nil}
+	return &Request{url, &req, map[string]string{}, map[string]string{}, defaultSetting, &resp, nil}
 }
 
-// Get returns *BeegoHttpRequest with GET method.
-func Get(url string) *BeegoHttpRequest {
+// Get returns *Request with GET method.
+func Get(url string) *Request {
 	return newBeegoRequest(url, "GET")
 }
 
-// Post returns *BeegoHttpRequest with POST method.
-func Post(url string) *BeegoHttpRequest {
+// Post returns *Request with POST method.
+func Post(url string) *Request {
 	return newBeegoRequest(url, "POST")
 }
 
-// Put returns *BeegoHttpRequest with PUT method.
-func Put(url string) *BeegoHttpRequest {
+// Put returns *Request with PUT method.
+func Put(url string) *Request {
 	return newBeegoRequest(url, "PUT")
 }
 
-// Delete returns *BeegoHttpRequest DELETE method.
-func Delete(url string) *BeegoHttpRequest {
+// Delete returns *Request DELETE method.
+func Delete(url string) *Request {
 	return newBeegoRequest(url, "DELETE")
 }
 
-// Head returns *BeegoHttpRequest with HEAD method.
-func Head(url string) *BeegoHttpRequest {
+// Head returns *Request with HEAD method.
+func Head(url string) *Request {
 	return newBeegoRequest(url, "HEAD")
 }
 
-// BeegoHttpSettings
-type BeegoHttpSettings struct {
+type Settings struct {
 	ShowDebug        bool
 	UserAgent        string
 	ConnectTimeout   time.Duration
@@ -101,93 +98,97 @@ type BeegoHttpSettings struct {
 	EnableCookie     bool
 }
 
-// BeegoHttpRequest provides more useful methods for requesting one url than http.Request.
-type BeegoHttpRequest struct {
+// HttpRequest provides more useful methods for requesting one url than http.Request.
+type Request struct {
 	url     string
 	req     *http.Request
 	params  map[string]string
 	files   map[string]string
-	setting BeegoHttpSettings
+	setting Settings
 	resp    *http.Response
 	body    []byte
 }
 
 // Change request settings
-func (b *BeegoHttpRequest) Setting(setting BeegoHttpSettings) *BeegoHttpRequest {
-	b.setting = setting
-	return b
+func (r *Request) Setting(setting Settings) *Request {
+	r.setting = setting
+	return r
 }
 
 // SetBasicAuth sets the request's Authorization header to use HTTP Basic Authentication with the provided username and password.
-func (b *BeegoHttpRequest) SetBasicAuth(username, password string) *BeegoHttpRequest {
-	b.req.SetBasicAuth(username, password)
-	return b
+func (r *Request) SetBasicAuth(username, password string) *Request {
+	r.req.SetBasicAuth(username, password)
+	return r
 }
 
 // SetEnableCookie sets enable/disable cookiejar
-func (b *BeegoHttpRequest) SetEnableCookie(enable bool) *BeegoHttpRequest {
-	b.setting.EnableCookie = enable
-	return b
+func (r *Request) SetEnableCookie(enable bool) *Request {
+	r.setting.EnableCookie = enable
+	return r
 }
 
 // SetUserAgent sets User-Agent header field
-func (b *BeegoHttpRequest) SetUserAgent(useragent string) *BeegoHttpRequest {
-	b.setting.UserAgent = useragent
-	return b
+func (r *Request) SetUserAgent(useragent string) *Request {
+	r.setting.UserAgent = useragent
+	return r
 }
 
 // Debug sets show debug or not when executing request.
-func (b *BeegoHttpRequest) Debug(isdebug bool) *BeegoHttpRequest {
-	b.setting.ShowDebug = isdebug
-	return b
+func (r *Request) Debug(isdebug bool) *Request {
+	r.setting.ShowDebug = isdebug
+	return r
 }
 
 // SetTimeout sets connect time out and read-write time out for BeegoRequest.
-func (b *BeegoHttpRequest) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *BeegoHttpRequest {
-	b.setting.ConnectTimeout = connectTimeout
-	b.setting.ReadWriteTimeout = readWriteTimeout
-	return b
+func (r *Request) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *Request {
+	r.setting.ConnectTimeout = connectTimeout
+	r.setting.ReadWriteTimeout = readWriteTimeout
+	return r
 }
 
 // SetTLSClientConfig sets tls connection configurations if visiting https url.
-func (b *BeegoHttpRequest) SetTLSClientConfig(config *tls.Config) *BeegoHttpRequest {
-	b.setting.TlsClientConfig = config
-	return b
+func (r *Request) SetTLSClientConfig(config *tls.Config) *Request {
+	r.setting.TlsClientConfig = config
+	return r
 }
 
 // Header add header item string in request.
-func (b *BeegoHttpRequest) Header(key, value string) *BeegoHttpRequest {
-	b.req.Header.Set(key, value)
-	return b
+func (r *Request) Header(key, value string) *Request {
+	r.req.Header.Set(key, value)
+	return r
+}
+
+func (r *Request) Headers() http.Header {
+	return r.req.Header
 }
 
 // Set the protocol version for incoming requests.
 // Client requests always use HTTP/1.1.
-func (b *BeegoHttpRequest) SetProtocolVersion(vers string) *BeegoHttpRequest {
+func (r *Request) SetProtocolVersion(vers string) *Request {
 	if len(vers) == 0 {
 		vers = "HTTP/1.1"
 	}
 
 	major, minor, ok := http.ParseHTTPVersion(vers)
 	if ok {
-		b.req.Proto = vers
-		b.req.ProtoMajor = major
-		b.req.ProtoMinor = minor
+		r.req.Proto = vers
+		r.req.ProtoMajor = major
+		r.req.ProtoMinor = minor
 	}
 
-	return b
+	return r
 }
 
 // SetCookie add cookie into request.
-func (b *BeegoHttpRequest) SetCookie(cookie *http.Cookie) *BeegoHttpRequest {
-	b.req.Header.Add("Cookie", cookie.String())
-	return b
+func (r *Request) SetCookie(cookie *http.Cookie) *Request {
+	r.req.Header.Add("Cookie", cookie.String())
+	return r
 }
 
 // Set transport to
-func (b *BeegoHttpRequest) SetTransport(transport http.RoundTripper) *BeegoHttpRequest {
-	b.setting.Transport = transport
-	return b
+func (r *Request) SetTransport(transport http.RoundTripper) *Request {
+	r.setting.Transport = transport
+	return r
 }
 
 // Set http proxy
@@ -197,47 +198,47 @@ func (b *BeegoHttpRequest) SetTransport(transport http.RoundTripper) *BeegoHttpR
 // 		u, _ := url.ParseRequestURI("http://127.0.0.1:8118")
 // 		return u, nil
 // 	}
-func (b *BeegoHttpRequest) SetProxy(proxy func(*http.Request) (*url.URL, error)) *BeegoHttpRequest {
-	b.setting.Proxy = proxy
-	return b
+func (r *Request) SetProxy(proxy func(*http.Request) (*url.URL, error)) *Request {
+	r.setting.Proxy = proxy
+	return r
 }
 
 // Param adds query param in to request.
 // params build query string as ?key1=value1&key2=value2...
-func (b *BeegoHttpRequest) Param(key, value string) *BeegoHttpRequest {
-	b.params[key] = value
-	return b
+func (r *Request) Param(key, value string) *Request {
+	r.params[key] = value
+	return r
 }
 
-func (b *BeegoHttpRequest) PostFile(formname, filename string) *BeegoHttpRequest {
-	b.files[formname] = filename
-	return b
+func (r *Request) PostFile(formname, filename string) *Request {
+	r.files[formname] = filename
+	return r
 }
 
 // Body adds request raw body.
 // it supports string and []byte.
-func (b *BeegoHttpRequest) Body(data interface{}) *BeegoHttpRequest {
+func (r *Request) Body(data interface{}) *Request {
 	switch t := data.(type) {
 	case string:
 		bf := bytes.NewBufferString(t)
-		b.req.Body = ioutil.NopCloser(bf)
-		b.req.ContentLength = int64(len(t))
+		r.req.Body = ioutil.NopCloser(bf)
+		r.req.ContentLength = int64(len(t))
 	case []byte:
 		bf := bytes.NewBuffer(t)
-		b.req.Body = ioutil.NopCloser(bf)
-		b.req.ContentLength = int64(len(t))
+		r.req.Body = ioutil.NopCloser(bf)
+		r.req.ContentLength = int64(len(t))
 	}
-	return b
+	return r
 }
 
-func (b *BeegoHttpRequest) getResponse() (*http.Response, error) {
-	if b.resp.StatusCode != 0 {
-		return b.resp, nil
+func (r *Request) getResponse() (*http.Response, error) {
+	if r.resp.StatusCode != 0 {
+		return r.resp, nil
 	}
 	var paramBody string
-	if len(b.params) > 0 {
+	if len(r.params) > 0 {
 		var buf bytes.Buffer
-		for k, v := range b.params {
+		for k, v := range r.params {
 			buf.WriteString(url.QueryEscape(k))
 			buf.WriteByte('=')
 			buf.WriteString(url.QueryEscape(v))
@@ -247,18 +248,18 @@ func (b *BeegoHttpRequest) getResponse() (*http.Response, error) {
 		paramBody = paramBody[0 : len(paramBody)-1]
 	}
 
-	if b.req.Method == "GET" && len(paramBody) > 0 {
-		if strings.Index(b.url, "?") != -1 {
-			b.url += "&" + paramBody
+	if r.req.Method == "GET" && len(paramBody) > 0 {
+		if strings.Index(r.url, "?") != -1 {
+			r.url += "&" + paramBody
 		} else {
-			b.url = b.url + "?" + paramBody
+			r.url = r.url + "?" + paramBody
 		}
-	} else if b.req.Method == "POST" && b.req.Body == nil {
-		if len(b.files) > 0 {
+	} else if r.req.Method == "POST" && r.req.Body == nil {
+		if len(r.files) > 0 {
 			pr, pw := io.Pipe()
 			bodyWriter := multipart.NewWriter(pw)
 			go func() {
-				for formname, filename := range b.files {
+				for formname, filename := range r.files {
 					fileWriter, err := bodyWriter.CreateFormFile(formname, filename)
 					if err != nil {
 						log.Fatal(err)
@@ -274,53 +275,53 @@ func (b *BeegoHttpRequest) getResponse() (*http.Response, error) {
 						log.Fatal(err)
 					}
 				}
-				for k, v := range b.params {
+				for k, v := range r.params {
 					bodyWriter.WriteField(k, v)
 				}
 				bodyWriter.Close()
 				pw.Close()
 			}()
-			b.Header("Content-Type", bodyWriter.FormDataContentType())
-			b.req.Body = ioutil.NopCloser(pr)
+			r.Header("Content-Type", bodyWriter.FormDataContentType())
+			r.req.Body = ioutil.NopCloser(pr)
 		} else if len(paramBody) > 0 {
-			b.Header("Content-Type", "application/x-www-form-urlencoded")
-			b.Body(paramBody)
+			r.Header("Content-Type", "application/x-www-form-urlencoded")
+			r.Body(paramBody)
 		}
 	}
 
-	url, err := url.Parse(b.url)
+	url, err := url.Parse(r.url)
 	if err != nil {
 		return nil, err
 	}
 
-	b.req.URL = url
+	r.req.URL = url
 
-	trans := b.setting.Transport
+	trans := r.setting.Transport
 
 	if trans == nil {
 		// create default transport
 		trans = &http.Transport{
-			TLSClientConfig: b.setting.TlsClientConfig,
-			Proxy:           b.setting.Proxy,
-			Dial:            TimeoutDialer(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout),
+			TLSClientConfig: r.setting.TlsClientConfig,
+			Proxy:           r.setting.Proxy,
+			Dial:            TimeoutDialer(r.setting.ConnectTimeout, r.setting.ReadWriteTimeout),
 		}
 	} else {
-		// if b.transport is *http.Transport then set the settings.
+		// if r.transport is *http.Transport then set the settings.
 		if t, ok := trans.(*http.Transport); ok {
 			if t.TLSClientConfig == nil {
-				t.TLSClientConfig = b.setting.TlsClientConfig
+				t.TLSClientConfig = r.setting.TlsClientConfig
 			}
 			if t.Proxy == nil {
-				t.Proxy = b.setting.Proxy
+				t.Proxy = r.setting.Proxy
 			}
 			if t.Dial == nil {
-				t.Dial = TimeoutDialer(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout)
+				t.Dial = TimeoutDialer(r.setting.ConnectTimeout, r.setting.ReadWriteTimeout)
 			}
 		}
 	}
 
 	var jar http.CookieJar
-	if b.setting.EnableCookie {
+	if r.setting.EnableCookie {
 		if defaultCookieJar == nil {
 			createDefaultCookie()
 		}
@@ -334,30 +335,30 @@ func (b *BeegoHttpRequest) getResponse() (*http.Response, error) {
 		Jar:       jar,
 	}
 
-	if len(b.setting.UserAgent) > 0 && len(b.req.Header.Get("User-Agent")) == 0 {
-		b.req.Header.Set("User-Agent", b.setting.UserAgent)
+	if len(r.setting.UserAgent) > 0 && len(r.req.Header.Get("User-Agent")) == 0 {
+		r.req.Header.Set("User-Agent", r.setting.UserAgent)
 	}
 
-	if b.setting.ShowDebug {
-		dump, err := httputil.DumpRequest(b.req, true)
+	if r.setting.ShowDebug {
+		dump, err := httputil.DumpRequest(r.req, true)
 		if err != nil {
 			println(err.Error())
 		}
 		println(string(dump))
 	}
 
-	resp, err := client.Do(b.req)
+	resp, err := client.Do(r.req)
 	if err != nil {
 		return nil, err
 	}
-	b.resp = resp
+	r.resp = resp
 	return resp, nil
 }
 
 // String returns the body string in response.
 // it calls Response inner.
-func (b *BeegoHttpRequest) String() (string, error) {
-	data, err := b.Bytes()
+func (r *Request) String() (string, error) {
+	data, err := r.Bytes()
 	if err != nil {
 		return "", err
 	}
@@ -367,11 +368,11 @@ func (b *BeegoHttpRequest) String() (string, error) {
 
 // Bytes returns the body []byte in response.
 // it calls Response inner.
-func (b *BeegoHttpRequest) Bytes() ([]byte, error) {
-	if b.body != nil {
-		return b.body, nil
+func (r *Request) Bytes() ([]byte, error) {
+	if r.body != nil {
+		return r.body, nil
 	}
-	resp, err := b.getResponse()
+	resp, err := r.getResponse()
 	if err != nil {
 		return nil, err
 	}
@@ -383,20 +384,20 @@ func (b *BeegoHttpRequest) Bytes() ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	b.body = data
+	r.body = data
 	return data, nil
 }
 
 // ToFile saves the body data in response to one file.
 // it calls Response inner.
-func (b *BeegoHttpRequest) ToFile(filename string) error {
+func (r *Request) ToFile(filename string) error {
 	f, err := os.Create(filename)
 	if err != nil {
 		return err
 	}
 	defer f.Close()
 
-	resp, err := b.getResponse()
+	resp, err := r.getResponse()
 	if err != nil {
 		return err
 	}
@@ -410,8 +411,8 @@ func (b *BeegoHttpRequest) ToFile(filename string) error {
 
 // ToJson returns the map that marshals from the body bytes as json in response .
 // it calls Response inner.
-func (b *BeegoHttpRequest) ToJson(v interface{}) error {
-	data, err := b.Bytes()
+func (r *Request) ToJson(v interface{}) error {
+	data, err := r.Bytes()
 	if err != nil {
 		return err
 	}
@@ -421,8 +422,8 @@ func (b *BeegoHttpRequest) ToJson(v interface{}) error {
 
 // ToXml returns the map that marshals from the body bytes as xml in response .
 // it calls Response inner.
-func (b *BeegoHttpRequest) ToXml(v interface{}) error {
-	data, err := b.Bytes()
+func (r *Request) ToXml(v interface{}) error {
+	data, err := r.Bytes()
 	if err != nil {
 		return err
 	}
@@ -431,8 +432,8 @@ func (b *BeegoHttpRequest) ToXml(v interface{}) error {
 }
 
 // Response executes request client gets response mannually.
-func (b *BeegoHttpRequest) Response() (*http.Response, error) {
-	return b.getResponse()
+func (r *Request) Response() (*http.Response, error) {
+	return r.getResponse()
 }
 
 // TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field.

+ 2 - 0
modules/setting/setting.go

@@ -82,6 +82,7 @@ var (
 		DeliverTimeout int
 		SkipTLSVerify  bool
 		Types          []string
+		PagingNum      int
 	}
 
 	// Repository settings.
@@ -601,6 +602,7 @@ func newWebhookService() {
 	Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5)
 	Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool()
 	Webhook.Types = []string{"gogs", "slack"}
+	Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10)
 }
 
 func NewServices() {

File diff suppressed because it is too large
+ 0 - 0
public/css/gogs.min.css


+ 5 - 4
public/js/gogs.js

@@ -341,10 +341,6 @@ function initRepository() {
 
     // Diff
     if ($('.repository.diff').length > 0) {
-        $('.diff-detail-box .toggle.button').click(function () {
-            $($(this).data('target')).slideToggle(100);
-        })
-
         var $counter = $('.diff-counter');
         if ($counter.length < 1) {
             return;
@@ -406,6 +402,11 @@ $(document).ready(function () {
             }
         }
     });
+    $('.tabular.menu .item').tab();
+
+    $('.toggle.button').click(function () {
+        $($(this).data('target')).slideToggle(100);
+    });
 
     // Dropzone
     if ($('#dropzone').length > 0) {

+ 17 - 0
public/less/_base.less

@@ -7,6 +7,19 @@ body {
 img {
 	border-radius: 3px;
 }
+pre {
+	font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+	&.raw {
+  	padding: 7px 12px;
+    margin: 10px 0;
+    background-color: #f8f8f8;
+    border: 1px solid #ddd;
+    border-radius: 3px;
+    font-size: 13px;
+    line-height: 1.5;
+    overflow: auto;
+	}
+}
 .full.height {
 	padding: 0;
   margin: 0 0 -@footer-margin*2 0;
@@ -121,6 +134,10 @@ img {
 		&.thin {
 			font-weight: normal;
 		}
+
+		&.middle {
+			vertical-align: middle;
+		}
 	}
 
 	.message {

+ 35 - 1
public/less/_repository.less

@@ -624,7 +624,7 @@
 		}
 	}
 	.hook.list {
-		.item:not(:first-child) {
+		> .item:not(:first-child) {
 			border-top: 1px solid #eaeaea;
 		}
 		.item {
@@ -632,6 +632,40 @@
 			.octicon,
 			.fa {
 				width: 20px;
+				text-align: center;
+			}
+		}
+	}
+	.hook.history.list {
+		.item {
+			padding-left: 13px;
+			.meta {
+				.ui.right {
+					margin-top: 5px;
+					.time {
+						font-size: 12px;
+					}
+				}
+			}
+			.info {
+				margin-top: 10px;
+				.tabular.menu {
+					.item {
+						font-weight: 500;
+					}
+				}
+				.tab.segment {
+					border: none;
+			    padding: 0;
+			    padding-top: 10px;
+			    box-shadow: none;
+			    * {
+			    	color: #666;
+			    }
+			    pre {
+						word-wrap: break-word;
+			    }
+				}
 			}
 		}
 	}

+ 1 - 1
routers/api/v1/repo_hooks.go

@@ -121,7 +121,7 @@ func CreateRepoHook(ctx *middleware.Context, form api.CreateHookOption) {
 // PATCH /repos/:username/:reponame/hooks/:id
 // https://developer.github.com/v3/repos/hooks/#edit-a-hook
 func EditRepoHook(ctx *middleware.Context, form api.EditHookOption) {
-	w, err := models.GetWebhookById(ctx.ParamsInt64(":id"))
+	w, err := models.GetWebhookByID(ctx.ParamsInt64(":id"))
 	if err != nil {
 		ctx.JSON(500, &base.ApiJsonErr{"GetWebhookById: " + err.Error(), base.DOC_URL})
 		return

+ 24 - 31
routers/repo/setting.go

@@ -417,15 +417,22 @@ func SlackHooksNewPost(ctx *middleware.Context, form auth.NewSlackHookForm) {
 	ctx.Redirect(orCtx.Link + "/settings/hooks")
 }
 
-func checkWebhook(ctx *middleware.Context) *models.Webhook {
-	w, err := models.GetWebhookById(ctx.ParamsInt64(":id"))
+func checkWebhook(ctx *middleware.Context) (*OrgRepoCtx, *models.Webhook) {
+	orCtx, err := getOrgRepoCtx(ctx)
+	if err != nil {
+		ctx.Handle(500, "getOrgRepoCtx", err)
+		return nil, nil
+	}
+	ctx.Data["BaseLink"] = orCtx.Link
+
+	w, err := models.GetWebhookByID(ctx.ParamsInt64(":id"))
 	if err != nil {
-		if err == models.ErrWebhookNotExist {
-			ctx.Handle(404, "GetWebhookById", nil)
+		if models.IsErrWebhookNotExist(err) {
+			ctx.Handle(404, "GetWebhookByID", nil)
 		} else {
-			ctx.Handle(500, "GetWebhookById", err)
+			ctx.Handle(500, "GetWebhookByID", err)
 		}
-		return nil
+		return nil, nil
 	}
 
 	switch w.HookTaskType {
@@ -436,7 +443,12 @@ func checkWebhook(ctx *middleware.Context) *models.Webhook {
 		ctx.Data["HookType"] = "gogs"
 	}
 	w.GetEvent()
-	return w
+
+	ctx.Data["History"], err = w.History(1)
+	if err != nil {
+		ctx.Handle(500, "History", err)
+	}
+	return orCtx, w
 }
 
 func WebHooksEdit(ctx *middleware.Context) {
@@ -444,17 +456,11 @@ func WebHooksEdit(ctx *middleware.Context) {
 	ctx.Data["PageIsSettingsHooks"] = true
 	ctx.Data["PageIsSettingsHooksEdit"] = true
 
-	ctx.Data["Webhook"] = checkWebhook(ctx)
+	orCtx, w := checkWebhook(ctx)
 	if ctx.Written() {
 		return
 	}
-
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.Handle(500, "getOrgRepoCtx", err)
-		return
-	}
-	ctx.Data["BaseLink"] = orCtx.Link
+	ctx.Data["Webhook"] = w
 
 	ctx.HTML(200, orCtx.NewTemplate)
 }
@@ -464,19 +470,12 @@ func WebHooksEditPost(ctx *middleware.Context, form auth.NewWebhookForm) {
 	ctx.Data["PageIsSettingsHooks"] = true
 	ctx.Data["PageIsSettingsHooksEdit"] = true
 
-	w := checkWebhook(ctx)
+	orCtx, w := checkWebhook(ctx)
 	if ctx.Written() {
 		return
 	}
 	ctx.Data["Webhook"] = w
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.Handle(500, "getOrgRepoCtx", err)
-		return
-	}
-	ctx.Data["BaseLink"] = orCtx.Link
-
 	if ctx.HasError() {
 		ctx.HTML(200, orCtx.NewTemplate)
 		return
@@ -511,19 +510,12 @@ func SlackHooksEditPost(ctx *middleware.Context, form auth.NewSlackHookForm) {
 	ctx.Data["PageIsSettingsHooks"] = true
 	ctx.Data["PageIsSettingsHooksEdit"] = true
 
-	w := checkWebhook(ctx)
+	orCtx, w := checkWebhook(ctx)
 	if ctx.Written() {
 		return
 	}
 	ctx.Data["Webhook"] = w
 
-	orCtx, err := getOrgRepoCtx(ctx)
-	if err != nil {
-		ctx.Handle(500, "getOrgRepoCtx", err)
-		return
-	}
-	ctx.Data["BaseLink"] = orCtx.Link
-
 	if ctx.HasError() {
 		ctx.HTML(200, orCtx.NewTemplate)
 		return
@@ -588,6 +580,7 @@ func TriggerHook(ctx *middleware.Context) {
 		return
 	}
 	models.HookQueue.AddRepoID(repo.ID)
+	ctx.Status(200)
 }
 
 func GitHooks(ctx *middleware.Context) {

+ 61 - 2
templates/repo/settings/hook_history.tmpl

@@ -2,7 +2,66 @@
 <h4 class="ui top attached header">
   {{.i18n.Tr "repo.settings.recent_deliveries"}}
 </h4>
-<div class="ui attached segment">
-	Coming soon!
+<div class="ui attached table segment">
+	<div class="ui hook history list">
+		{{range .History}}
+		<div class="item">
+			<div class="meta">
+				{{if .IsSucceed}}
+				<span class="text green"><i class="octicon octicon-check"></i></span>
+				{{else}}
+				<span class="text red"><i class="octicon octicon-alert"></i></span>
+				{{end}}
+				<a class="ui blue sha label toggle button" data-target="#info-{{.ID}}">{{.UUID}}</a>
+				<div class="ui right">
+					<span class="text grey time">
+						{{.DeliveredString}}
+					</span>
+				</div>
+			</div>
+			<div class="info hide" id="info-{{.ID}}">
+				<div class="ui top attached tabular menu">
+				  <a class="item active" data-tab="request-{{.ID}}">{{$.i18n.Tr "repo.settings.webhook.request"}}</a>
+				  <a class="item" data-tab="response-{{.ID}}">
+				  	{{$.i18n.Tr "repo.settings.webhook.response"}}
+				  	{{if .ResponseInfo}}
+					  	{{if .IsSucceed}}
+					  	<span class="ui green label">{{.ResponseInfo.Status}}</span>
+					  	{{else}}
+					  	<span class="ui red label">500</span>
+					  	{{end}}
+				  	{{else}}
+				  		<span class="ui label">N/A</span>
+				  	{{end}}
+				  </a>
+				</div>
+				<div class="ui bottom attached tab segment active" data-tab="request-{{.ID}}">
+				  {{if .RequestInfo}}
+				  <h5>{{$.i18n.Tr "repo.settings.webhook.headers"}}</h5>
+				  <pre class="raw"><strong>Request URL:</strong> {{.URL}}
+<strong>Request method:</strong> POST
+{{ range $key, $val := .RequestInfo.Headers }}<strong>{{$key}}:</strong> {{$val}}
+{{end}}</pre>
+					<h5>{{$.i18n.Tr "repo.settings.webhook.payload"}}</h5>
+					<pre class="raw">{{.PayloadContent}}</pre>
+				  {{else}}
+				  N/A
+				  {{end}}
+				</div>
+				<div class="ui bottom attached tab segment" data-tab="response-{{.ID}}">
+				  {{if .ResponseInfo}}
+				  <h5>{{$.i18n.Tr "repo.settings.webhook.headers"}}</h5>
+				  <pre class="raw">{{ range $key, $val := .ResponseInfo.Headers }}<strong>{{$key}}:</strong> {{$val}}
+{{end}}</pre>
+					<h5>{{$.i18n.Tr "repo.settings.webhook.body"}}</h5>
+					<pre class="raw">{{.ResponseInfo.Body}}</pre>
+				  {{else}}
+				  N/A
+				  {{end}}
+				</div>
+			</div>
+		</div>
+		{{end}}
+	</div>
 </div>
 {{end}}

+ 1 - 1
templates/user/settings/applications.tmpl

@@ -19,7 +19,7 @@
             {{range .Tokens}}
             <div class="item ui grid">
               <div class="one wide column">
-                <i class="ssh-key-state-indicator fa fa-circle{{if .HasRecentActivity}} active invert poping up{{else}}-o{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.token_state_desc"}}" data-variation="inverted"{{end}}></i>
+                <i class="ssh-key-state-indicator fa fa-circle{{if .HasRecentActivity}} active invert poping up{{else}}-o{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.token_state_desc"}}" data-variation="inverted tiny"{{end}}></i>
               </div>
               <div class="one wide column">
                 <i class="fa fa-send fa-2x left"></i>

+ 1 - 1
templates/user/settings/sshkeys.tmpl

@@ -19,7 +19,7 @@
             {{range .Keys}}
             <div class="item ui grid">
               <div class="one wide column">
-                <i class="ssh-key-state-indicator fa fa-circle{{if .HasRecentActivity}} active invert poping up{{else}}-o{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted"{{end}}></i>
+                <i class="ssh-key-state-indicator fa fa-circle{{if .HasRecentActivity}} active invert poping up{{else}}-o{{end}}" {{if .HasRecentActivity}}data-content="{{$.i18n.Tr "settings.key_state_desc"}}" data-variation="inverted tiny"{{end}}></i>
               </div>
               <div class="one wide column">
                 <i class="mega-octicon octicon-key left"></i>

Some files were not shown because too many files changed in this diff