Skip to content

Commit fa4663e

Browse files
lafrikstechknowlogick
authored andcommitted
Add push webhook support for mirrored repositories (#4127)
1 parent bf55276 commit fa4663e

File tree

8 files changed

+257
-19
lines changed

8 files changed

+257
-19
lines changed

Gopkg.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

models/action.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const (
4747
ActionReopenPullRequest // 15
4848
ActionDeleteTag // 16
4949
ActionDeleteBranch // 17
50+
ActionMirrorSyncPush // 18
51+
ActionMirrorSyncCreate // 19
52+
ActionMirrorSyncDelete // 20
5053
)
5154

5255
var (
@@ -736,6 +739,71 @@ func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error
736739
return mergePullRequestAction(x, actUser, repo, pull)
737740
}
738741

742+
func mirrorSyncAction(e Engine, opType ActionType, repo *Repository, refName string, data []byte) error {
743+
if err := notifyWatchers(e, &Action{
744+
ActUserID: repo.OwnerID,
745+
ActUser: repo.MustOwner(),
746+
OpType: opType,
747+
RepoID: repo.ID,
748+
Repo: repo,
749+
IsPrivate: repo.IsPrivate,
750+
RefName: refName,
751+
Content: string(data),
752+
}); err != nil {
753+
return fmt.Errorf("notifyWatchers: %v", err)
754+
}
755+
return nil
756+
}
757+
758+
// MirrorSyncPushActionOptions mirror synchronization action options.
759+
type MirrorSyncPushActionOptions struct {
760+
RefName string
761+
OldCommitID string
762+
NewCommitID string
763+
Commits *PushCommits
764+
}
765+
766+
// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
767+
func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
768+
if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
769+
opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
770+
}
771+
772+
apiCommits := opts.Commits.ToAPIPayloadCommits(repo.HTMLURL())
773+
774+
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
775+
apiPusher := repo.MustOwner().APIFormat()
776+
if err := PrepareWebhooks(repo, HookEventPush, &api.PushPayload{
777+
Ref: opts.RefName,
778+
Before: opts.OldCommitID,
779+
After: opts.NewCommitID,
780+
CompareURL: setting.AppURL + opts.Commits.CompareURL,
781+
Commits: apiCommits,
782+
Repo: repo.APIFormat(AccessModeOwner),
783+
Pusher: apiPusher,
784+
Sender: apiPusher,
785+
}); err != nil {
786+
return fmt.Errorf("PrepareWebhooks: %v", err)
787+
}
788+
789+
data, err := json.Marshal(opts.Commits)
790+
if err != nil {
791+
return err
792+
}
793+
794+
return mirrorSyncAction(x, ActionMirrorSyncPush, repo, opts.RefName, data)
795+
}
796+
797+
// MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
798+
func MirrorSyncCreateAction(repo *Repository, refName string) error {
799+
return mirrorSyncAction(x, ActionMirrorSyncCreate, repo, refName, nil)
800+
}
801+
802+
// MirrorSyncDeleteAction adds new action for mirror synchronization of delete reference.
803+
func MirrorSyncDeleteAction(repo *Repository, refName string) error {
804+
return mirrorSyncAction(x, ActionMirrorSyncDelete, repo, refName, nil)
805+
}
806+
739807
// GetFeedsOptions options for retrieving feeds
740808
type GetFeedsOptions struct {
741809
RequestedUser *User

models/repo_mirror.go

Lines changed: 136 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Copyright 2016 The Gogs Authors. All rights reserved.
2+
// Copyright 2018 The Gitea Authors. All rights reserved.
23
// Use of this source code is governed by a MIT-style
34
// license that can be found in the LICENSE file.
45

56
package models
67

78
import (
89
"fmt"
10+
"strings"
911
"time"
1012

1113
"code.gitea.io/git"
@@ -119,8 +121,68 @@ func (m *Mirror) SaveAddress(addr string) error {
119121
return cfg.SaveToIndent(configPath, "\t")
120122
}
121123

124+
// gitShortEmptySha Git short empty SHA
125+
const gitShortEmptySha = "0000000"
126+
127+
// mirrorSyncResult contains information of a updated reference.
128+
// If the oldCommitID is "0000000", it means a new reference, the value of newCommitID is empty.
129+
// If the newCommitID is "0000000", it means the reference is deleted, the value of oldCommitID is empty.
130+
type mirrorSyncResult struct {
131+
refName string
132+
oldCommitID string
133+
newCommitID string
134+
}
135+
136+
// parseRemoteUpdateOutput detects create, update and delete operations of references from upstream.
137+
func parseRemoteUpdateOutput(output string) []*mirrorSyncResult {
138+
results := make([]*mirrorSyncResult, 0, 3)
139+
lines := strings.Split(output, "\n")
140+
for i := range lines {
141+
// Make sure reference name is presented before continue
142+
idx := strings.Index(lines[i], "-> ")
143+
if idx == -1 {
144+
continue
145+
}
146+
147+
refName := lines[i][idx+3:]
148+
149+
switch {
150+
case strings.HasPrefix(lines[i], " * "): // New reference
151+
results = append(results, &mirrorSyncResult{
152+
refName: refName,
153+
oldCommitID: gitShortEmptySha,
154+
})
155+
case strings.HasPrefix(lines[i], " - "): // Delete reference
156+
results = append(results, &mirrorSyncResult{
157+
refName: refName,
158+
newCommitID: gitShortEmptySha,
159+
})
160+
case strings.HasPrefix(lines[i], " "): // New commits of a reference
161+
delimIdx := strings.Index(lines[i][3:], " ")
162+
if delimIdx == -1 {
163+
log.Error(2, "SHA delimiter not found: %q", lines[i])
164+
continue
165+
}
166+
shas := strings.Split(lines[i][3:delimIdx+3], "..")
167+
if len(shas) != 2 {
168+
log.Error(2, "Expect two SHAs but not what found: %q", lines[i])
169+
continue
170+
}
171+
results = append(results, &mirrorSyncResult{
172+
refName: refName,
173+
oldCommitID: shas[0],
174+
newCommitID: shas[1],
175+
})
176+
177+
default:
178+
log.Warn("parseRemoteUpdateOutput: unexpected update line %q", lines[i])
179+
}
180+
}
181+
return results
182+
}
183+
122184
// runSync returns true if sync finished without error.
123-
func (m *Mirror) runSync() bool {
185+
func (m *Mirror) runSync() ([]*mirrorSyncResult, bool) {
124186
repoPath := m.Repo.RepoPath()
125187
wikiPath := m.Repo.WikiPath()
126188
timeout := time.Duration(setting.Git.Timeout.Mirror) * time.Second
@@ -130,28 +192,30 @@ func (m *Mirror) runSync() bool {
130192
gitArgs = append(gitArgs, "--prune")
131193
}
132194

133-
if _, stderr, err := process.GetManager().ExecDir(
195+
_, stderr, err := process.GetManager().ExecDir(
134196
timeout, repoPath, fmt.Sprintf("Mirror.runSync: %s", repoPath),
135-
"git", gitArgs...); err != nil {
197+
"git", gitArgs...)
198+
if err != nil {
136199
// sanitize the output, since it may contain the remote address, which may
137200
// contain a password
138201
message, err := sanitizeOutput(stderr, repoPath)
139202
if err != nil {
140203
log.Error(4, "sanitizeOutput: %v", err)
141-
return false
204+
return nil, false
142205
}
143206
desc := fmt.Sprintf("Failed to update mirror repository '%s': %s", repoPath, message)
144207
log.Error(4, desc)
145208
if err = CreateRepositoryNotice(desc); err != nil {
146209
log.Error(4, "CreateRepositoryNotice: %v", err)
147210
}
148-
return false
211+
return nil, false
149212
}
213+
output := stderr
150214

151215
gitRepo, err := git.OpenRepository(repoPath)
152216
if err != nil {
153217
log.Error(4, "OpenRepository: %v", err)
154-
return false
218+
return nil, false
155219
}
156220
if err = SyncReleasesWithTags(m.Repo, gitRepo); err != nil {
157221
log.Error(4, "Failed to synchronize tags to releases for repository: %v", err)
@@ -170,29 +234,29 @@ func (m *Mirror) runSync() bool {
170234
message, err := sanitizeOutput(stderr, wikiPath)
171235
if err != nil {
172236
log.Error(4, "sanitizeOutput: %v", err)
173-
return false
237+
return nil, false
174238
}
175239
desc := fmt.Sprintf("Failed to update mirror wiki repository '%s': %s", wikiPath, message)
176240
log.Error(4, desc)
177241
if err = CreateRepositoryNotice(desc); err != nil {
178242
log.Error(4, "CreateRepositoryNotice: %v", err)
179243
}
180-
return false
244+
return nil, false
181245
}
182246
}
183247

184248
branches, err := m.Repo.GetBranches()
185249
if err != nil {
186250
log.Error(4, "GetBranches: %v", err)
187-
return false
251+
return nil, false
188252
}
189253

190254
for i := range branches {
191255
cache.Remove(m.Repo.GetCommitsCountCacheKey(branches[i].Name, true))
192256
}
193257

194258
m.UpdatedUnix = util.TimeStampNow()
195-
return true
259+
return parseRemoteUpdateOutput(output), true
196260
}
197261

198262
func getMirrorByRepoID(e Engine, repoID int64) (*Mirror, error) {
@@ -268,7 +332,8 @@ func SyncMirrors() {
268332
continue
269333
}
270334

271-
if !m.runSync() {
335+
results, ok := m.runSync()
336+
if !ok {
272337
continue
273338
}
274339

@@ -278,6 +343,66 @@ func SyncMirrors() {
278343
continue
279344
}
280345

346+
var gitRepo *git.Repository
347+
if len(results) == 0 {
348+
log.Trace("SyncMirrors [repo_id: %d]: no commits fetched", m.RepoID)
349+
} else {
350+
gitRepo, err = git.OpenRepository(m.Repo.RepoPath())
351+
if err != nil {
352+
log.Error(2, "OpenRepository [%d]: %v", m.RepoID, err)
353+
continue
354+
}
355+
}
356+
357+
for _, result := range results {
358+
// Discard GitHub pull requests, i.e. refs/pull/*
359+
if strings.HasPrefix(result.refName, "refs/pull/") {
360+
continue
361+
}
362+
363+
// Create reference
364+
if result.oldCommitID == gitShortEmptySha {
365+
if err = MirrorSyncCreateAction(m.Repo, result.refName); err != nil {
366+
log.Error(2, "MirrorSyncCreateAction [repo_id: %d]: %v", m.RepoID, err)
367+
}
368+
continue
369+
}
370+
371+
// Delete reference
372+
if result.newCommitID == gitShortEmptySha {
373+
if err = MirrorSyncDeleteAction(m.Repo, result.refName); err != nil {
374+
log.Error(2, "MirrorSyncDeleteAction [repo_id: %d]: %v", m.RepoID, err)
375+
}
376+
continue
377+
}
378+
379+
// Push commits
380+
oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID)
381+
if err != nil {
382+
log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
383+
continue
384+
}
385+
newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID)
386+
if err != nil {
387+
log.Error(2, "GetFullCommitID [%d]: %v", m.RepoID, err)
388+
continue
389+
}
390+
commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID)
391+
if err != nil {
392+
log.Error(2, "CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err)
393+
continue
394+
}
395+
if err = MirrorSyncPushAction(m.Repo, MirrorSyncPushActionOptions{
396+
RefName: result.refName,
397+
OldCommitID: oldCommitID,
398+
NewCommitID: newCommitID,
399+
Commits: ListToPushCommits(commits),
400+
}); err != nil {
401+
log.Error(2, "MirrorSyncPushAction [repo_id: %d]: %v", m.RepoID, err)
402+
continue
403+
}
404+
}
405+
281406
// Get latest commit date and update to current repository updated time
282407
commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath())
283408
if err != nil {

modules/templates/helper.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,8 @@ func ActionIcon(opType models.ActionType) string {
391391
return "issue-closed"
392392
case models.ActionReopenIssue, models.ActionReopenPullRequest:
393393
return "issue-reopened"
394+
case models.ActionMirrorSyncPush, models.ActionMirrorSyncCreate, models.ActionMirrorSyncDelete:
395+
return "repo-clone"
394396
default:
395397
return "invalid type"
396398
}

options/locale/locale_en-US.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,9 @@ push_tag = pushed tag <a href="%s/src/tag/%s">%[2]s</a> to <a href="%[1]s">%[3]s
16631663
delete_tag = deleted tag %[2]s from <a href="%[1]s">%[3]s</a>
16641664
delete_branch = deleted branch %[2]s from <a href="%[1]s">%[3]s</a>
16651665
compare_commits = Compare %d commits
1666+
mirror_sync_push = synced commits to <a href="%[1]s/src/%[2]s">%[3]s</a> at <a href="%[1]s">%[4]s</a> from mirror
1667+
mirror_sync_create = synced new reference <a href="%s/src/%s">%[2]s</a> to <a href="%[1]s">%[3]s</a> from mirror
1668+
mirror_sync_delete = synced and deleted reference <code>%[2]s</code> at <a href="%[1]s">%[3]s</a> from mirror
16661669

16671670
[tool]
16681671
ago = %s ago

templates/user/dashboard/feeds.tmpl

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</div>
66
<div class="ui grid">
77
<div class="ui thirteen wide column">
8-
<div class="{{if eq .GetOpType 5}}push news{{end}}">
8+
<div class="{{if or (eq .GetOpType 5) (eq .GetOpType 18)}}push news{{end}}">
99
<p>
1010
<a href="{{AppSubUrl}}/{{.GetActUserName}}" title="{{.GetActFullName}}">{{.ShortActUserName}}</a>
1111
{{if eq .GetOpType 1}}
@@ -49,9 +49,16 @@
4949
{{else if eq .GetOpType 17}}
5050
{{ $index := index .GetIssueInfos 0}}
5151
{{$.i18n.Tr "action.delete_branch" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
52+
{{else if eq .GetOpType 18}}
53+
{{ $branchLink := .GetBranch | EscapePound}}
54+
{{$.i18n.Tr "action.mirror_sync_push" .GetRepoLink $branchLink .GetBranch .ShortRepoPath | Str2html}}
55+
{{else if eq .GetOpType 19}}
56+
{{$.i18n.Tr "action.mirror_sync_create" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
57+
{{else if eq .GetOpType 20}}
58+
{{$.i18n.Tr "action.mirror_sync_delete" .GetRepoLink .GetBranch .ShortRepoPath | Str2html}}
5259
{{end}}
5360
</p>
54-
{{if eq .GetOpType 5}}
61+
{{if or (eq .GetOpType 5) (eq .GetOpType 18)}}
5562
<div class="content">
5663
<ul>
5764
{{ $push := ActionContent2Commits .}}

0 commit comments

Comments
 (0)