Browse Source

webhook: add fork event

Unknwon 8 years ago
parent
commit
b06f299748

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

@@ -758,6 +758,8 @@ settings.event_create = Create
 settings.event_create_desc = Branch or tag created
 settings.event_delete = Delete
 settings.event_delete_desc = Branch or tag deleted
+settings.event_fork = Fork
+settings.event_fork_desc = Repository forked
 settings.event_pull_request = Pull Request
 settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, or synchronized.
 settings.event_push = Push
@@ -1208,6 +1210,7 @@ notices.delete_success = System notices have been deleted successfully.
 
 [action]
 create_repo = created repository <a href="%s">%s</a>
+fork_repo = forked a repository to <a href="%s">%s</a>
 rename_repo = renamed repository from <code>%[1]s</code> to <a href="%[2]s">%[3]s</a>
 commit_repo = pushed to <a href="%[1]s/src/%[2]s">%[3]s</a> at <a href="%[1]s">%[4]s</a>
 compare_commits = View comparison for these %d commits

+ 34 - 27
models/action.go

@@ -46,6 +46,7 @@ const (
 	ACTION_CREATE_BRANCH                             // 16
 	ACTION_DELETE_BRANCH                             // 17
 	ACTION_DELETE_TAG                                // 18
+	ACTION_FORK_REPO                                 // 19
 )
 
 var (
@@ -177,20 +178,20 @@ func (a *Action) GetIssueContent() string {
 }
 
 func newRepoAction(e Engine, doer, owner *User, repo *Repository) (err error) {
-	if err = notifyWatchers(e, &Action{
+	opType := ACTION_CREATE_REPO
+	if repo.IsFork {
+		opType = ACTION_FORK_REPO
+	}
+
+	return notifyWatchers(e, &Action{
 		ActUserID:    doer.ID,
 		ActUserName:  doer.Name,
-		OpType:       ACTION_CREATE_REPO,
+		OpType:       opType,
 		RepoID:       repo.ID,
 		RepoUserName: repo.Owner.Name,
 		RepoName:     repo.Name,
 		IsPrivate:    repo.IsPrivate,
-	}); err != nil {
-		return fmt.Errorf("notify watchers '%d/%d': %v", owner.ID, repo.ID, err)
-	}
-
-	log.Trace("action.newRepoAction: %s/%s", owner.Name, repo.Name)
-	return err
+	})
 }
 
 // NewRepoAction adds new action for creating repository.
@@ -489,12 +490,6 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 		return fmt.Errorf("Marshal: %v", err)
 	}
 
-	defer func() {
-		// It's safe to fail when the whole function is called during hook execution
-		// because resource released after exit.
-		go HookQueue.Add(repo.ID)
-	}()
-
 	refName := git.RefEndName(opts.RefFullName)
 	action := &Action{
 		ActUserID:    pusher.ID,
@@ -512,9 +507,6 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 	switch opType {
 	case ACTION_COMMIT_REPO: // Push
 		if isDelRef {
-			action.OpType = ACTION_DELETE_BRANCH
-			MustNotifyWatchers(action)
-
 			if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
 				Ref:        refName,
 				RefType:    "branch",
@@ -525,15 +517,17 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 				return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
 			}
 
+			action.OpType = ACTION_DELETE_BRANCH
+			if err = NotifyWatchers(action); err != nil {
+				return fmt.Errorf("NotifyWatchers.(delete branch): %v", err)
+			}
+
 			// Delete branch doesn't have anything to push or compare
 			return nil
 		}
 
 		compareURL := setting.AppUrl + opts.Commits.CompareURL
 		if isNewRef {
-			action.OpType = ACTION_CREATE_BRANCH
-			MustNotifyWatchers(action)
-
 			compareURL = ""
 			if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
 				Ref:           refName,
@@ -544,10 +538,13 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 			}); err != nil {
 				return fmt.Errorf("PrepareWebhooks.(new branch): %v", err)
 			}
+
+			action.OpType = ACTION_CREATE_BRANCH
+			if err = NotifyWatchers(action); err != nil {
+				return fmt.Errorf("NotifyWatchers.(new branch): %v", err)
+			}
 		}
 
-		action.OpType = ACTION_COMMIT_REPO
-		MustNotifyWatchers(action)
 		if err = PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
 			Ref:        opts.RefFullName,
 			Before:     opts.OldCommitID,
@@ -561,11 +558,13 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 			return fmt.Errorf("PrepareWebhooks.(new commit): %v", err)
 		}
 
+		action.OpType = ACTION_COMMIT_REPO
+		if err = NotifyWatchers(action); err != nil {
+			return fmt.Errorf("NotifyWatchers.(new commit): %v", err)
+		}
+
 	case ACTION_PUSH_TAG: // Tag
 		if isDelRef {
-			action.OpType = ACTION_DELETE_TAG
-			MustNotifyWatchers(action)
-
 			if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
 				Ref:        refName,
 				RefType:    "tag",
@@ -575,11 +574,14 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 			}); err != nil {
 				return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
 			}
+
+			action.OpType = ACTION_DELETE_TAG
+			if err = NotifyWatchers(action); err != nil {
+				return fmt.Errorf("NotifyWatchers.(delete tag): %v", err)
+			}
 			return nil
 		}
 
-		action.OpType = ACTION_PUSH_TAG
-		MustNotifyWatchers(action)
 		if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
 			Ref:           refName,
 			RefType:       "tag",
@@ -589,6 +591,11 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
 		}); err != nil {
 			return fmt.Errorf("PrepareWebhooks.(new tag): %v", err)
 		}
+
+		action.OpType = ACTION_PUSH_TAG
+		if err = NotifyWatchers(action); err != nil {
+			return fmt.Errorf("NotifyWatchers.(new tag): %v", err)
+		}
 	}
 
 	return nil

+ 4 - 4
models/org.go

@@ -20,8 +20,8 @@ var (
 )
 
 // IsOwnedBy returns true if given user is in the owner team.
-func (org *User) IsOwnedBy(uid int64) bool {
-	return IsOrganizationOwner(org.ID, uid)
+func (org *User) IsOwnedBy(userID int64) bool {
+	return IsOrganizationOwner(org.ID, userID)
 }
 
 // IsOrgMember returns true if given user is member of organization.
@@ -246,8 +246,8 @@ type OrgUser struct {
 }
 
 // IsOrganizationOwner returns true if given user is in the owner team.
-func IsOrganizationOwner(orgId, uid int64) bool {
-	has, _ := x.Where("is_owner=?", true).And("uid=?", uid).And("org_id=?", orgId).Get(new(OrgUser))
+func IsOrganizationOwner(orgID, userID int64) bool {
+	has, _ := x.Where("is_owner = ?", true).And("uid = ?", userID).And("org_id = ?", orgID).Get(new(OrgUser))
 	return has
 }
 

+ 26 - 24
models/repo.go

@@ -2068,24 +2068,27 @@ func (repo *Repository) GetWatchers(page int) ([]*User, error) {
 
 func notifyWatchers(e Engine, act *Action) error {
 	// Add feeds for user self and all watchers.
-	watches, err := getWatchers(e, act.RepoID)
+	watchers, err := getWatchers(e, act.RepoID)
 	if err != nil {
-		return fmt.Errorf("get watchers: %v", err)
+		return fmt.Errorf("getWatchers: %v", err)
 	}
 
+	// Reset ID to reuse Action object
+	act.ID = 0
+
 	// Add feed for actioner.
 	act.UserID = act.ActUserID
 	if _, err = e.Insert(act); err != nil {
-		return fmt.Errorf("insert new actioner: %v", err)
+		return fmt.Errorf("insert new action: %v", err)
 	}
 
-	for i := range watches {
-		if act.ActUserID == watches[i].UserID {
+	for i := range watchers {
+		if act.ActUserID == watchers[i].UserID {
 			continue
 		}
 
 		act.ID = 0
-		act.UserID = watches[i].UserID
+		act.UserID = watchers[i].UserID
 		if _, err = e.Insert(act); err != nil {
 			return fmt.Errorf("insert new action: %v", err)
 		}
@@ -2098,13 +2101,6 @@ func NotifyWatchers(act *Action) error {
 	return notifyWatchers(x, act)
 }
 
-func MustNotifyWatchers(act *Action) {
-	act.ID = 0 // Reset ID to reuse Action object
-	if err := NotifyWatchers(act); err != nil {
-		log.Error(2, "NotifyWatchers: %v", err)
-	}
-}
-
 //   _________ __
 //  /   _____//  |______ _______
 //  \_____  \\   __\__  \\_  __ \
@@ -2168,24 +2164,26 @@ func (repo *Repository) GetStargazers(page int) ([]*User, error) {
 //  \___  / \____/|__|  |__|_ \
 //      \/                   \/
 
-// HasForkedRepo checks if given user has already forked a repository with given ID.
+// HasForkedRepo checks if given user has already forked a repository.
+// When user has already forked, it returns true along with the repository.
 func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) {
 	repo := new(Repository)
-	has, _ := x.Where("owner_id=? AND fork_id=?", ownerID, repoID).Get(repo)
+	has, _ := x.Where("owner_id = ? AND fork_id = ?", ownerID, repoID).Get(repo)
 	return repo, has
 }
 
-func ForkRepository(doer, owner *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) {
+// ForkRepository creates a fork of target repository under another user domain.
+func ForkRepository(doer, owner *User, baseRepo *Repository, name, desc string) (_ *Repository, err error) {
 	repo := &Repository{
 		OwnerID:       owner.ID,
 		Owner:         owner,
 		Name:          name,
 		LowerName:     strings.ToLower(name),
 		Description:   desc,
-		DefaultBranch: oldRepo.DefaultBranch,
-		IsPrivate:     oldRepo.IsPrivate,
+		DefaultBranch: baseRepo.DefaultBranch,
+		IsPrivate:     baseRepo.IsPrivate,
 		IsFork:        true,
-		ForkID:        oldRepo.ID,
+		ForkID:        baseRepo.ID,
 	}
 
 	sess := x.NewSession()
@@ -2196,16 +2194,14 @@ func ForkRepository(doer, owner *User, oldRepo *Repository, name, desc string) (
 
 	if err = createRepository(sess, doer, owner, repo); err != nil {
 		return nil, err
-	}
-
-	if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", oldRepo.ID); err != nil {
+	} else if _, err = sess.Exec("UPDATE `repository` SET num_forks=num_forks+1 WHERE id=?", baseRepo.ID); err != nil {
 		return nil, err
 	}
 
-	repoPath := RepoPath(owner.Name, repo.Name)
+	repoPath := repo.repoPath(sess)
 	_, stderr, err := process.ExecTimeout(10*time.Minute,
 		fmt.Sprintf("ForkRepository 'git clone': %s/%s", owner.Name, repo.Name),
-		"git", "clone", "--bare", oldRepo.RepoPath(), repoPath)
+		"git", "clone", "--bare", baseRepo.RepoPath(), repoPath)
 	if err != nil {
 		return nil, fmt.Errorf("git clone: %v", stderr)
 	}
@@ -2219,6 +2215,12 @@ func ForkRepository(doer, owner *User, oldRepo *Repository, name, desc string) (
 
 	if err = createDelegateHooks(repoPath); err != nil {
 		return nil, fmt.Errorf("createDelegateHooks: %v", err)
+	} else if err = prepareWebhooks(sess, baseRepo, HOOK_EVENT_FORK, &api.ForkPayload{
+		Forkee: repo.APIFormat(nil),
+		Repo:   baseRepo.APIFormat(nil),
+		Sender: doer.APIFormat(),
+	}); err != nil {
+		return nil, fmt.Errorf("prepareWebhooks: %v", err)
 	}
 
 	return repo, sess.Commit()

+ 49 - 27
models/webhook.go

@@ -64,6 +64,7 @@ func IsValidHookContentType(name string) bool {
 type HookEvents struct {
 	Create      bool `json:"create"`
 	Delete      bool `json:"delete"`
+	Fork        bool `json:"fork"`
 	Push        bool `json:"push"`
 	PullRequest bool `json:"pull_request"`
 }
@@ -163,6 +164,12 @@ func (w *Webhook) HasDeleteEvent() bool {
 		(w.ChooseEvents && w.HookEvents.Delete)
 }
 
+// HasForkEvent returns true if hook enabled fork event.
+func (w *Webhook) HasForkEvent() bool {
+	return w.SendEverything ||
+		(w.ChooseEvents && w.HookEvents.Fork)
+}
+
 // HasPushEvent returns true if hook enabled push event.
 func (w *Webhook) HasPushEvent() bool {
 	return w.PushOnly || w.SendEverything ||
@@ -176,15 +183,21 @@ func (w *Webhook) HasPullRequestEvent() bool {
 }
 
 func (w *Webhook) EventsArray() []string {
-	events := make([]string, 0, 3)
+	events := make([]string, 0, 5)
 	if w.HasCreateEvent() {
-		events = append(events, "create")
+		events = append(events, string(HOOK_EVENT_CREATE))
+	}
+	if w.HasDeleteEvent() {
+		events = append(events, string(HOOK_EVENT_DELETE))
+	}
+	if w.HasForkEvent() {
+		events = append(events, string(HOOK_EVENT_FORK))
 	}
 	if w.HasPushEvent() {
-		events = append(events, "push")
+		events = append(events, string(HOOK_EVENT_PUSH))
 	}
 	if w.HasPullRequestEvent() {
-		events = append(events, "pull_request")
+		events = append(events, string(HOOK_EVENT_PULL_REQUEST))
 	}
 	return events
 }
@@ -232,10 +245,10 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
 	})
 }
 
-// GetActiveWebhooksByRepoID returns all active webhooks of repository.
-func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
+// getActiveWebhooksByRepoID returns all active webhooks of repository.
+func getActiveWebhooksByRepoID(e Engine, repoID int64) ([]*Webhook, error) {
 	webhooks := make([]*Webhook, 0, 5)
-	return webhooks, x.Where("repo_id = ?", repoID).And("is_active = ?", true).Find(&webhooks)
+	return webhooks, e.Where("repo_id = ?", repoID).And("is_active = ?", true).Find(&webhooks)
 }
 
 // GetWebhooksByRepoID returns all webhooks of a repository.
@@ -290,10 +303,10 @@ func GetWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
 	return ws, err
 }
 
-// GetActiveWebhooksByOrgID returns all active webhooks for an organization.
-func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
-	err = x.Where("org_id=?", orgID).And("is_active=?", true).Find(&ws)
-	return ws, err
+// getActiveWebhooksByOrgID returns all active webhooks for an organization.
+func getActiveWebhooksByOrgID(e Engine, orgID int64) ([]*Webhook, error) {
+	ws := make([]*Webhook, 3)
+	return ws, e.Where("org_id=?", orgID).And("is_active=?", true).Find(&ws)
 }
 
 //   ___ ___                __   ___________              __
@@ -345,6 +358,7 @@ type HookEventType string
 const (
 	HOOK_EVENT_CREATE       HookEventType = "create"
 	HOOK_EVENT_DELETE       HookEventType = "delete"
+	HOOK_EVENT_FORK         HookEventType = "fork"
 	HOOK_EVENT_PUSH         HookEventType = "push"
 	HOOK_EVENT_PULL_REQUEST HookEventType = "pull_request"
 )
@@ -438,16 +452,16 @@ func HookTasks(hookID int64, page int) ([]*HookTask, error) {
 	return tasks, x.Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).Where("hook_id=?", hookID).Desc("id").Find(&tasks)
 }
 
-// CreateHookTask creates a new hook task,
+// createHookTask creates a new hook task,
 // it handles conversion from Payload to PayloadContent.
-func CreateHookTask(t *HookTask) error {
+func createHookTask(e Engine, t *HookTask) error {
 	data, err := t.Payloader.JSONPayload()
 	if err != nil {
 		return err
 	}
 	t.UUID = gouuid.NewV4().String()
 	t.PayloadContent = string(data)
-	_, err = x.Insert(t)
+	_, err = e.Insert(t)
 	return err
 }
 
@@ -457,8 +471,8 @@ func UpdateHookTask(t *HookTask) error {
 	return err
 }
 
-// prepareWebhooks adds list of webhooks to task queue.
-func prepareWebhooks(repo *Repository, event HookEventType, p api.Payloader, webhooks []*Webhook) (err error) {
+// prepareHookTasks adds list of webhooks to task queue.
+func prepareHookTasks(e Engine, repo *Repository, event HookEventType, p api.Payloader, webhooks []*Webhook) (err error) {
 	if len(webhooks) == 0 {
 		return nil
 	}
@@ -511,7 +525,7 @@ func prepareWebhooks(repo *Repository, event HookEventType, p api.Payloader, web
 			signature = hex.EncodeToString(sig.Sum(nil))
 		}
 
-		if err = CreateHookTask(&HookTask{
+		if err = createHookTask(e, &HookTask{
 			RepoID:      repo.ID,
 			HookID:      w.ID,
 			Type:        w.HookTaskType,
@@ -522,29 +536,37 @@ func prepareWebhooks(repo *Repository, event HookEventType, p api.Payloader, web
 			EventType:   event,
 			IsSSL:       w.IsSSL,
 		}); err != nil {
-			return fmt.Errorf("CreateHookTask: %v", err)
+			return fmt.Errorf("createHookTask: %v", err)
 		}
 	}
+
+	// It's safe to fail when the whole function is called during hook execution
+	// because resource released after exit.
+	go HookQueue.Add(repo.ID)
 	return nil
 }
 
-// PrepareWebhooks adds all active webhooks to task queue.
-func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
-	webhooks, err := GetActiveWebhooksByRepoID(repo.ID)
+func prepareWebhooks(e Engine, repo *Repository, event HookEventType, p api.Payloader) error {
+	webhooks, err := getActiveWebhooksByRepoID(e, repo.ID)
 	if err != nil {
-		return fmt.Errorf("GetActiveWebhooksByRepoID [%d]: %v", repo.ID, err)
+		return fmt.Errorf("getActiveWebhooksByRepoID [%d]: %v", repo.ID, err)
 	}
 
 	// check if repo belongs to org and append additional webhooks
-	if repo.MustOwner().IsOrganization() {
+	if repo.mustOwner(e).IsOrganization() {
 		// get hooks for org
-		orgws, err := GetActiveWebhooksByOrgID(repo.OwnerID)
+		orgws, err := getActiveWebhooksByOrgID(e, repo.OwnerID)
 		if err != nil {
-			return fmt.Errorf("GetActiveWebhooksByOrgID [%d]: %v", repo.OwnerID, err)
+			return fmt.Errorf("getActiveWebhooksByOrgID [%d]: %v", repo.OwnerID, err)
 		}
 		webhooks = append(webhooks, orgws...)
 	}
-	return prepareWebhooks(repo, event, p, webhooks)
+	return prepareHookTasks(e, repo, event, p, webhooks)
+}
+
+// PrepareWebhooks adds all active webhooks to task queue.
+func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
+	return prepareWebhooks(x, repo, event, p)
 }
 
 // TestWebhook adds the test webhook matches the ID to task queue.
@@ -553,7 +575,7 @@ func TestWebhook(repo *Repository, event HookEventType, p api.Payloader, webhook
 	if err != nil {
 		return fmt.Errorf("GetWebhookOfRepoByID [repo_id: %d, id: %d]: %v", repo.ID, webhookID, err)
 	}
-	return prepareWebhooks(repo, event, p, []*Webhook{webhook})
+	return prepareHookTasks(x, repo, event, p, []*Webhook{webhook})
 }
 
 func (t *HookTask) deliver() {

+ 18 - 1
models/webhook_discord.go

@@ -74,7 +74,6 @@ func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
 	repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
 	content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
-
 	return &DiscordPayload{
 		Embeds: []*DiscordEmbedObject{{
 			Description: content,
@@ -92,7 +91,23 @@ func getDiscordDeletePayload(p *api.DeletePayload) (*DiscordPayload, error) {
 	refName := git.RefEndName(p.Ref)
 	repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
 	content := fmt.Sprintf("Deleted %s: %s/%s", p.RefType, repoLink, refName)
+	return &DiscordPayload{
+		Embeds: []*DiscordEmbedObject{{
+			Description: content,
+			URL:         setting.AppUrl + p.Sender.UserName,
+			Author: &DiscordEmbedAuthorObject{
+				Name:    p.Sender.UserName,
+				IconURL: p.Sender.AvatarUrl,
+			},
+		}},
+	}, nil
+}
 
+// getDiscordForkPayload composes Discord payload for forked by a repository.
+func getDiscordForkPayload(p *api.ForkPayload) (*DiscordPayload, error) {
+	baseLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+	forkLink := DiscordLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+	content := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
 	return &DiscordPayload{
 		Embeds: []*DiscordEmbedObject{{
 			Description: content,
@@ -230,6 +245,8 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (paylo
 		payload, err = getDiscordCreatePayload(p.(*api.CreatePayload))
 	case HOOK_EVENT_DELETE:
 		payload, err = getDiscordDeletePayload(p.(*api.DeletePayload))
+	case HOOK_EVENT_FORK:
+		payload, err = getDiscordForkPayload(p.(*api.ForkPayload))
 	case HOOK_EVENT_PUSH:
 		payload, err = getDiscordPushPayload(p.(*api.PushPayload), slack)
 	case HOOK_EVENT_PULL_REQUEST:

+ 12 - 0
models/webhook_slack.go

@@ -90,6 +90,16 @@ func getSlackDeletePayload(p *api.DeletePayload) (*SlackPayload, error) {
 	}, nil
 }
 
+// getSlackForkPayload composes Slack payload for forked by a repository.
+func getSlackForkPayload(p *api.ForkPayload) (*SlackPayload, error) {
+	baseLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
+	forkLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
+	text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
+	return &SlackPayload{
+		Text: text,
+	}, nil
+}
+
 func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
 	// n new commits
 	var (
@@ -194,6 +204,8 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (payload
 		payload, err = getSlackCreatePayload(p.(*api.CreatePayload))
 	case HOOK_EVENT_DELETE:
 		payload, err = getSlackDeletePayload(p.(*api.DeletePayload))
+	case HOOK_EVENT_FORK:
+		payload, err = getSlackForkPayload(p.(*api.ForkPayload))
 	case HOOK_EVENT_PUSH:
 		payload, err = getSlackPushPayload(p.(*api.PushPayload), slack)
 	case HOOK_EVENT_PULL_REQUEST:

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


+ 2 - 1
modules/form/repo.go

@@ -23,7 +23,7 @@ import (
 //         \/        \/                   \/        \/                        \/       \/ \/
 
 type CreateRepo struct {
-	Uid         int64  `binding:"Required"`
+	UserID      int64  `binding:"Required"`
 	RepoName    string `binding:"Required;AlphaDashDot;MaxSize(100)"`
 	Private     bool
 	Description string `binding:"MaxSize(255)"`
@@ -136,6 +136,7 @@ type Webhook struct {
 	Events      string
 	Create      bool
 	Delete      bool
+	Fork        bool
 	Push        bool
 	PullRequest bool
 	Active      bool

+ 2 - 0
modules/template/template.go

@@ -262,6 +262,8 @@ func ActionIcon(opType int) string {
 		return "git-branch"
 	case 17, 18: // Delete branch or tag
 		return "alert"
+	case 19: // Fork a repository
+		return "repo-forked"
 	default:
 		return "invalid type"
 	}

+ 23 - 29
routers/repo/pull.go

@@ -38,31 +38,27 @@ var (
 	}
 )
 
-func getForkRepository(ctx *context.Context) *models.Repository {
-	forkRepo, err := models.GetRepositoryByID(ctx.ParamsInt64(":repoid"))
+func parseBaseRepository(ctx *context.Context) *models.Repository {
+	baseRepo, err := models.GetRepositoryByID(ctx.ParamsInt64(":repoid"))
 	if err != nil {
-		if models.IsErrRepoNotExist(err) {
-			ctx.Handle(404, "GetRepositoryByID", nil)
-		} else {
-			ctx.Handle(500, "GetRepositoryByID", err)
-		}
+		ctx.NotFoundOrServerError("GetRepositoryByID", models.IsErrRepoNotExist, err)
 		return nil
 	}
 
-	if !forkRepo.CanBeForked() || !forkRepo.HasAccess(ctx.User.ID) {
-		ctx.Handle(404, "getForkRepository", nil)
+	if !baseRepo.CanBeForked() || !baseRepo.HasAccess(ctx.User.ID) {
+		ctx.NotFound()
 		return nil
 	}
 
-	ctx.Data["repo_name"] = forkRepo.Name
-	ctx.Data["description"] = forkRepo.Description
-	ctx.Data["IsPrivate"] = forkRepo.IsPrivate
+	ctx.Data["repo_name"] = baseRepo.Name
+	ctx.Data["description"] = baseRepo.Description
+	ctx.Data["IsPrivate"] = baseRepo.IsPrivate
 
-	if err = forkRepo.GetOwner(); err != nil {
+	if err = baseRepo.GetOwner(); err != nil {
 		ctx.Handle(500, "GetOwner", err)
 		return nil
 	}
-	ctx.Data["ForkFrom"] = forkRepo.Owner.Name + "/" + forkRepo.Name
+	ctx.Data["ForkFrom"] = baseRepo.Owner.Name + "/" + baseRepo.Name
 
 	if err := ctx.User.GetOrganizations(true); err != nil {
 		ctx.Handle(500, "GetOrganizations", err)
@@ -70,13 +66,13 @@ func getForkRepository(ctx *context.Context) *models.Repository {
 	}
 	ctx.Data["Orgs"] = ctx.User.Orgs
 
-	return forkRepo
+	return baseRepo
 }
 
 func Fork(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("new_fork")
 
-	getForkRepository(ctx)
+	parseBaseRepository(ctx)
 	if ctx.Written() {
 		return
 	}
@@ -88,12 +84,12 @@ func Fork(ctx *context.Context) {
 func ForkPost(ctx *context.Context, f form.CreateRepo) {
 	ctx.Data["Title"] = ctx.Tr("new_fork")
 
-	forkRepo := getForkRepository(ctx)
+	baseRepo := parseBaseRepository(ctx)
 	if ctx.Written() {
 		return
 	}
 
-	ctxUser := checkContextUser(ctx, f.Uid)
+	ctxUser := checkContextUser(ctx, f.UserID)
 	if ctx.Written() {
 		return
 	}
@@ -104,27 +100,25 @@ func ForkPost(ctx *context.Context, f form.CreateRepo) {
 		return
 	}
 
-	repo, has := models.HasForkedRepo(ctxUser.ID, forkRepo.ID)
+	repo, has := models.HasForkedRepo(ctxUser.ID, baseRepo.ID)
 	if has {
-		ctx.Redirect(setting.AppSubUrl + "/" + ctxUser.Name + "/" + repo.Name)
+		ctx.Redirect(repo.Link())
 		return
 	}
 
 	// Check ownership of organization.
-	if ctxUser.IsOrganization() {
-		if !ctxUser.IsOwnedBy(ctx.User.ID) {
-			ctx.Error(403)
-			return
-		}
+	if ctxUser.IsOrganization() && !ctxUser.IsOwnedBy(ctx.User.ID) {
+		ctx.Error(403)
+		return
 	}
 
 	// Cannot fork to same owner
-	if ctxUser.ID == forkRepo.OwnerID {
+	if ctxUser.ID == baseRepo.OwnerID {
 		ctx.RenderWithErr(ctx.Tr("repo.settings.cannot_fork_to_same_owner"), FORK, &f)
 		return
 	}
 
-	repo, err := models.ForkRepository(ctx.User, ctxUser, forkRepo, f.RepoName, f.Description)
+	repo, err := models.ForkRepository(ctx.User, ctxUser, baseRepo, f.RepoName, f.Description)
 	if err != nil {
 		ctx.Data["Err_RepoName"] = true
 		switch {
@@ -140,8 +134,8 @@ func ForkPost(ctx *context.Context, f form.CreateRepo) {
 		return
 	}
 
-	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
-	ctx.Redirect(setting.AppSubUrl + "/" + ctxUser.Name + "/" + repo.Name)
+	log.Trace("Repository forked from '%s' -> '%s'", baseRepo.FullName(), repo.FullName())
+	ctx.Redirect(repo.Link())
 }
 
 func checkPullInfo(ctx *context.Context) *models.Issue {

+ 1 - 1
routers/repo/repo.go

@@ -109,7 +109,7 @@ func CreatePost(ctx *context.Context, f form.CreateRepo) {
 	ctx.Data["Licenses"] = models.Licenses
 	ctx.Data["Readmes"] = models.Readmes
 
-	ctxUser := checkContextUser(ctx, f.Uid)
+	ctxUser := checkContextUser(ctx, f.UserID)
 	if ctx.Written() {
 		return
 	}

+ 1 - 0
routers/repo/webhook.go

@@ -111,6 +111,7 @@ func ParseHookEvent(f form.Webhook) *models.HookEvent {
 		HookEvents: models.HookEvents{
 			Create:      f.Create,
 			Delete:      f.Delete,
+			Fork:        f.Fork,
 			Push:        f.Push,
 			PullRequest: f.PullRequest,
 		},

+ 1 - 1
templates/repo/create.tmpl

@@ -12,7 +12,7 @@
 					<div class="inline required field {{if .Err_Owner}}error{{end}}">
 						<label>{{.i18n.Tr "repo.owner"}}</label>
 						<div class="ui selection owner dropdown">
-							<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
+							<input type="hidden" id="user_id" name="user_id" value="{{.ContextUser.ID}}" required>
 							<span class="text">
 								<img class="ui mini image" src="{{.ContextUser.RelAvatarLink}}">
 								{{.ContextUser.ShortName 20}}

+ 1 - 1
templates/repo/pulls/fork.tmpl

@@ -12,7 +12,7 @@
 					<div class="inline required field {{if .Err_Owner}}error{{end}}">
 						<label>{{.i18n.Tr "repo.owner"}}</label>
 						<div class="ui selection owner dropdown">
-							<input type="hidden" id="uid" name="uid" value="{{.ContextUser.ID}}" required>
+							<input type="hidden" id="user_id" name="user_id" value="{{.ContextUser.ID}}" required>
 							<span class="text">
 								<img class="ui mini image" src="{{.ContextUser.RelAvatarLink}}">
 								{{.ContextUser.ShortName 20}}

+ 10 - 0
templates/repo/settings/webhook_settings.tmpl

@@ -42,6 +42,16 @@
 				</div>
 			</div>
 		</div>
+		<!-- Fork -->
+		<div class="seven wide column">
+			<div class="field">
+				<div class="ui checkbox">
+					<input class="hidden" name="fork" type="checkbox" tabindex="0" {{if .Webhook.Fork}}checked{{end}}>
+					<label>{{.i18n.Tr "repo.settings.event_fork"}}</label>
+					<span class="help">{{.i18n.Tr "repo.settings.event_fork_desc"}}</span>
+				</div>
+			</div>
+		</div>
 		<!-- Push -->
 		<div class="seven wide column">
 			<div class="field">

+ 2 - 0
templates/user/dashboard/feeds.tmpl

@@ -51,6 +51,8 @@
 							{{$.i18n.Tr "action.delete_branch" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
 						{{else if eq .GetOpType 18}}
 							{{$.i18n.Tr "action.delete_tag" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
+						{{else if eq .GetOpType 19}}
+							{{$.i18n.Tr "action.fork_repo" .GetRepoLink .ShortRepoPath | Str2html}}
 						{{end}}
 					</p>
 					{{if eq .GetOpType 5}}

+ 17 - 0
vendor/github.com/gogits/go-gogs-client/repo_hook.go

@@ -160,6 +160,23 @@ func (p *DeletePayload) JSONPayload() ([]byte, error) {
 	return json.MarshalIndent(p, "", "  ")
 }
 
+// ___________           __
+// \_   _____/__________|  | __
+//  |    __)/  _ \_  __ \  |/ /
+//  |     \(  <_> )  | \/    <
+//  \___  / \____/|__|  |__|_ \
+//      \/                   \/
+
+type ForkPayload struct {
+	Forkee *Repository `json:"forkee"`
+	Repo   *Repository `json:"repository"`
+	Sender *User       `json:"sender"`
+}
+
+func (p *ForkPayload) JSONPayload() ([]byte, error) {
+	return json.MarshalIndent(p, "", "  ")
+}
+
 // __________             .__
 // \______   \__ __  _____|  |__
 //  |     ___/  |  \/  ___/  |  \

+ 3 - 3
vendor/vendor.json

@@ -165,10 +165,10 @@
 			"revisionTime": "2017-02-19T18:16:29Z"
 		},
 		{
-			"checksumSHA1": "rtJ+nZ9VHh2X2Zon7wLczPAAc/s=",
+			"checksumSHA1": "sAGNvN2IXzD+rra6Y9sxJBpR4L8=",
 			"path": "github.com/gogits/go-gogs-client",
-			"revision": "ba630f557c8349952183305373fa89b155202bac",
-			"revisionTime": "2017-02-24T20:25:47Z"
+			"revision": "264a3d5bc98e108f17cc055338dbf4b94faf0d21",
+			"revisionTime": "2017-02-25T08:33:02Z"
 		},
 		{
 			"checksumSHA1": "p4yoFWgDiTfpu1JYgh26t6+VDTk=",

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