Browse Source

webhook: able to detect delete branch or tag (#2315)

pull/4240/head
Unknwon 8 years ago
parent
commit
f0086e66ae
No known key found for this signature in database
GPG Key ID: 25B575AE3213B2B3
  1. 4
      conf/locale/locale_en-US.ini
  2. 58
      models/action.go
  3. 29
      models/update.go
  4. 12
      models/webhook.go
  5. 53
      models/webhook_discord.go
  6. 43
      models/webhook_slack.go
  7. 4
      modules/bindata/bindata.go
  8. 1
      modules/form/repo.go
  9. 1
      routers/repo/webhook.go
  10. 10
      templates/repo/settings/webhook_settings.tmpl
  11. 2
      vendor/github.com/gogits/go-gogs-client/gogs.go
  12. 27
      vendor/github.com/gogits/go-gogs-client/repo_hook.go
  13. 6
      vendor/vendor.json

4
conf/locale/locale_en-US.ini

@ -755,7 +755,9 @@ settings.event_push_only = Just the <code>push</code> event.
settings.event_send_everything = I need <strong>everything</strong>. settings.event_send_everything = I need <strong>everything</strong>.
settings.event_choose = Let me choose what I need. settings.event_choose = Let me choose what I need.
settings.event_create = Create settings.event_create = Create
settings.event_create_desc = Branch, or tag created settings.event_create_desc = Branch or tag created
settings.event_delete = Delete
settings.event_delete_desc = Branch or tag deleted
settings.event_pull_request = Pull Request 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_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, or synchronized.
settings.event_push = Push settings.event_push = Push

58
models/action.go

@ -458,18 +458,16 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
return fmt.Errorf("UpdateRepository: %v", err) return fmt.Errorf("UpdateRepository: %v", err)
} }
isNewBranch := false isNewRef := opts.OldCommitID == git.EMPTY_SHA
isDelRef := opts.NewCommitID == git.EMPTY_SHA
opType := ACTION_COMMIT_REPO opType := ACTION_COMMIT_REPO
// Check it's tag push or branch. // Check if it's tag push or branch.
if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) { if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
opType = ACTION_PUSH_TAG opType = ACTION_PUSH_TAG
opts.Commits = &PushCommits{}
} else { } else {
// TODO: detect branch deletion
// if not the first commit, set the compare URL. // if not the first commit, set the compare URL.
if opts.OldCommitID == git.EMPTY_SHA { if !isNewRef && !isDelRef {
isNewBranch = true
} else {
opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID) opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
} }
@ -506,20 +504,36 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
go HookQueue.Add(repo.ID) go HookQueue.Add(repo.ID)
}() }()
apiPusher := pusher.APIFormat()
apiRepo := repo.APIFormat(nil) apiRepo := repo.APIFormat(nil)
apiPusher := pusher.APIFormat()
switch opType { switch opType {
case ACTION_COMMIT_REPO: // Push case ACTION_COMMIT_REPO: // Push
if isDelRef {
if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
Ref: refName,
RefType: "branch",
PusherType: api.PUSHER_TYPE_USER,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
}
// Delete branch doesn't have anything to push or compare
return nil
}
compareURL := setting.AppUrl + opts.Commits.CompareURL compareURL := setting.AppUrl + opts.Commits.CompareURL
if isNewBranch { if isNewRef {
compareURL = "" compareURL = ""
if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{ if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
Ref: refName, Ref: refName,
RefType: "branch", RefType: "branch",
DefaultBranch: repo.DefaultBranch,
Repo: apiRepo, Repo: apiRepo,
Sender: apiPusher, Sender: apiPusher,
}); err != nil { }); err != nil {
return fmt.Errorf("PrepareWebhooks (new branch): %v", err) return fmt.Errorf("PrepareWebhooks.(new branch): %v", err)
} }
} }
@ -533,16 +547,32 @@ func CommitRepoAction(opts CommitRepoActionOptions) error {
Pusher: apiPusher, Pusher: apiPusher,
Sender: apiPusher, Sender: apiPusher,
}); err != nil { }); err != nil {
return fmt.Errorf("PrepareWebhooks (new commit): %v", err) return fmt.Errorf("PrepareWebhooks.(new commit): %v", err)
} }
case ACTION_PUSH_TAG: // Create case ACTION_PUSH_TAG: // Tag
return PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{ if isDelRef {
if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
Ref: refName, Ref: refName,
RefType: "tag", RefType: "tag",
PusherType: api.PUSHER_TYPE_USER,
Repo: apiRepo, Repo: apiRepo,
Sender: apiPusher, Sender: apiPusher,
}) }); err != nil {
return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
}
return nil
}
if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
Ref: refName,
RefType: "tag",
DefaultBranch: repo.DefaultBranch,
Repo: apiRepo,
Sender: apiPusher,
}); err != nil {
return fmt.Errorf("PrepareWebhooks.(new tag): %v", err)
}
} }
return nil return nil

29
models/update.go

@ -10,8 +10,6 @@ import (
"os/exec" "os/exec"
"strings" "strings"
log "gopkg.in/clog.v1"
git "github.com/gogits/git-module" git "github.com/gogits/git-module"
) )
@ -29,6 +27,10 @@ func CommitToPushCommit(commit *git.Commit) *PushCommit {
} }
func ListToPushCommits(l *list.List) *PushCommits { func ListToPushCommits(l *list.List) *PushCommits {
if l == nil {
return &PushCommits{}
}
commits := make([]*PushCommit, 0) commits := make([]*PushCommit, 0)
var actEmail string var actEmail string
for e := l.Front(); e != nil; e = e.Next() { for e := l.Front(); e != nil; e = e.Next() {
@ -68,12 +70,6 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
return fmt.Errorf("Fail to call 'git update-server-info': %v", err) return fmt.Errorf("Fail to call 'git update-server-info': %v", err)
} }
if isDelRef {
log.Trace("Reference '%s' has been deleted from '%s/%s' by %s",
opts.RefFullName, opts.RepoUserName, opts.RepoName, opts.PusherName)
return nil
}
gitRepo, err := git.OpenRepository(repoPath) gitRepo, err := git.OpenRepository(repoPath)
if err != nil { if err != nil {
return fmt.Errorf("OpenRepository: %v", err) return fmt.Errorf("OpenRepository: %v", err)
@ -100,27 +96,30 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
NewCommitID: opts.NewCommitID, NewCommitID: opts.NewCommitID,
Commits: &PushCommits{}, Commits: &PushCommits{},
}); err != nil { }); err != nil {
return fmt.Errorf("CommitRepoAction (tag): %v", err) return fmt.Errorf("CommitRepoAction.(tag): %v", err)
} }
return nil return nil
} }
var l *list.List
// Skip read parent commits when delete branch
if !isDelRef {
// Push new branch.
newCommit, err := gitRepo.GetCommit(opts.NewCommitID) newCommit, err := gitRepo.GetCommit(opts.NewCommitID)
if err != nil { if err != nil {
return fmt.Errorf("gitRepo.GetCommit: %v", err) return fmt.Errorf("GetCommit [commit_id: %s]: %v", opts.NewCommitID, err)
} }
// Push new branch.
var l *list.List
if isNewRef { if isNewRef {
l, err = newCommit.CommitsBeforeLimit(10) l, err = newCommit.CommitsBeforeLimit(10)
if err != nil { if err != nil {
return fmt.Errorf("newCommit.CommitsBeforeLimit: %v", err) return fmt.Errorf("CommitsBeforeLimit [commit_id: %s]: %v", newCommit.ID, err)
} }
} else { } else {
l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID) l, err = newCommit.CommitsBeforeUntil(opts.OldCommitID)
if err != nil { if err != nil {
return fmt.Errorf("newCommit.CommitsBeforeUntil: %v", err) return fmt.Errorf("CommitsBeforeUntil [commit_id: %s]: %v", opts.OldCommitID, err)
}
} }
} }
@ -133,7 +132,7 @@ func PushUpdate(opts PushUpdateOptions) (err error) {
NewCommitID: opts.NewCommitID, NewCommitID: opts.NewCommitID,
Commits: ListToPushCommits(l), Commits: ListToPushCommits(l),
}); err != nil { }); err != nil {
return fmt.Errorf("CommitRepoAction (branch): %v", err) return fmt.Errorf("CommitRepoAction.(branch): %v", err)
} }
return nil return nil
} }

12
models/webhook.go

@ -63,6 +63,7 @@ func IsValidHookContentType(name string) bool {
type HookEvents struct { type HookEvents struct {
Create bool `json:"create"` Create bool `json:"create"`
Delete bool `json:"delete"`
Push bool `json:"push"` Push bool `json:"push"`
PullRequest bool `json:"pull_request"` PullRequest bool `json:"pull_request"`
} }
@ -156,6 +157,12 @@ func (w *Webhook) HasCreateEvent() bool {
(w.ChooseEvents && w.HookEvents.Create) (w.ChooseEvents && w.HookEvents.Create)
} }
// HasDeleteEvent returns true if hook enabled delete event.
func (w *Webhook) HasDeleteEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Delete)
}
// HasPushEvent returns true if hook enabled push event. // HasPushEvent returns true if hook enabled push event.
func (w *Webhook) HasPushEvent() bool { func (w *Webhook) HasPushEvent() bool {
return w.PushOnly || w.SendEverything || return w.PushOnly || w.SendEverything ||
@ -337,6 +344,7 @@ type HookEventType string
const ( const (
HOOK_EVENT_CREATE HookEventType = "create" HOOK_EVENT_CREATE HookEventType = "create"
HOOK_EVENT_DELETE HookEventType = "delete"
HOOK_EVENT_PUSH HookEventType = "push" HOOK_EVENT_PUSH HookEventType = "push"
HOOK_EVENT_PULL_REQUEST HookEventType = "pull_request" HOOK_EVENT_PULL_REQUEST HookEventType = "pull_request"
) )
@ -462,6 +470,10 @@ func prepareWebhooks(repo *Repository, event HookEventType, p api.Payloader, web
if !w.HasCreateEvent() { if !w.HasCreateEvent() {
continue continue
} }
case HOOK_EVENT_DELETE:
if !w.HasDeleteEvent() {
continue
}
case HOOK_EVENT_PUSH: case HOOK_EVENT_PUSH:
if !w.HasPushEvent() { if !w.HasPushEvent() {
continue continue

53
models/webhook_discord.go

@ -68,22 +68,35 @@ func DiscordSHALinkFormatter(url string, text string) string {
return fmt.Sprintf("[`%s`](%s)", text, url) return fmt.Sprintf("[`%s`](%s)", text, url)
} }
func getDiscordCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*DiscordPayload, error) { // getDiscordCreatePayload composes Discord payload for create new branch or tag.
// Created tag/branch func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
refName := git.RefEndName(p.Ref) refName := git.RefEndName(p.Ref)
repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName) refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink) content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
return &DiscordPayload{ return &DiscordPayload{
Username: slack.Username,
AvatarURL: slack.IconURL,
Embeds: []*DiscordEmbedObject{{ Embeds: []*DiscordEmbedObject{{
Description: content, Description: content,
URL: setting.AppUrl + p.Sender.UserName, URL: setting.AppUrl + p.Sender.UserName,
Color: int(color), Author: &DiscordEmbedAuthorObject{
Name: p.Sender.UserName,
IconURL: p.Sender.AvatarUrl,
},
}},
}, nil
}
// getDiscordDeletePayload composes Discord payload for delete a branch or tag.
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{ Author: &DiscordEmbedAuthorObject{
Name: p.Sender.UserName, Name: p.Sender.UserName,
IconURL: p.Sender.AvatarUrl, IconURL: p.Sender.AvatarUrl,
@ -206,22 +219,32 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (
}, nil }, nil
} }
func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) { func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (payload *DiscordPayload, err error) {
d := new(DiscordPayload)
slack := &SlackMeta{} slack := &SlackMeta{}
if err := json.Unmarshal([]byte(meta), &slack); err != nil { if err := json.Unmarshal([]byte(meta), &slack); err != nil {
return d, fmt.Errorf("GetDiscordPayload meta json: %v", err) return nil, fmt.Errorf("json.Unmarshal: %v", err)
} }
switch event { switch event {
case HOOK_EVENT_CREATE: case HOOK_EVENT_CREATE:
return getDiscordCreatePayload(p.(*api.CreatePayload), slack) payload, err = getDiscordCreatePayload(p.(*api.CreatePayload))
case HOOK_EVENT_DELETE:
payload, err = getDiscordDeletePayload(p.(*api.DeletePayload))
case HOOK_EVENT_PUSH: case HOOK_EVENT_PUSH:
return getDiscordPushPayload(p.(*api.PushPayload), slack) payload, err = getDiscordPushPayload(p.(*api.PushPayload), slack)
case HOOK_EVENT_PULL_REQUEST: case HOOK_EVENT_PULL_REQUEST:
return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), slack) payload, err = getDiscordPullRequestPayload(p.(*api.PullRequestPayload), slack)
}
if err != nil {
return nil, fmt.Errorf("event '%s': %v", event, err)
}
payload.Username = slack.Username
payload.AvatarURL = slack.IconURL
if len(payload.Embeds) > 0 {
color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
payload.Embeds[0].Color = int(color)
} }
return d, nil return payload, nil
} }

43
models/webhook_slack.go

@ -69,19 +69,24 @@ func SlackLinkFormatter(url string, text string) string {
return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text)) return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text))
} }
func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) { // getSlackCreatePayload composes Slack payload for create new branch or tag.
// Created tag/branch func getSlackCreatePayload(p *api.CreatePayload) (*SlackPayload, error) {
refName := git.RefEndName(p.Ref) refName := git.RefEndName(p.Ref)
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name) repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
refLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName) refLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName) text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
return &SlackPayload{
Text: text,
}, nil
}
// getSlackDeletePayload composes Slack payload for delete a branch or tag.
func getSlackDeletePayload(p *api.DeletePayload) (*SlackPayload, error) {
refName := git.RefEndName(p.Ref)
repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
return &SlackPayload{ return &SlackPayload{
Channel: slack.Channel,
Text: text, Text: text,
Username: slack.Username,
IconURL: slack.IconURL,
}, nil }, nil
} }
@ -178,22 +183,32 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S
}, nil }, nil
} }
func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) { func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (payload *SlackPayload, err error) {
s := new(SlackPayload)
slack := &SlackMeta{} slack := &SlackMeta{}
if err := json.Unmarshal([]byte(meta), &slack); err != nil { if err := json.Unmarshal([]byte(meta), &slack); err != nil {
return s, fmt.Errorf("GetSlackPayload meta json: %v", err) return nil, fmt.Errorf("json.Unmarshal: %v", err)
} }
switch event { switch event {
case HOOK_EVENT_CREATE: case HOOK_EVENT_CREATE:
return getSlackCreatePayload(p.(*api.CreatePayload), slack) payload, err = getSlackCreatePayload(p.(*api.CreatePayload))
case HOOK_EVENT_DELETE:
payload, err = getSlackDeletePayload(p.(*api.DeletePayload))
case HOOK_EVENT_PUSH: case HOOK_EVENT_PUSH:
return getSlackPushPayload(p.(*api.PushPayload), slack) payload, err = getSlackPushPayload(p.(*api.PushPayload), slack)
case HOOK_EVENT_PULL_REQUEST: case HOOK_EVENT_PULL_REQUEST:
return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) payload, err = getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
}
if err != nil {
return nil, fmt.Errorf("event '%s': %v", event, err)
}
payload.Channel = slack.Channel
payload.Username = slack.Username
payload.IconURL = slack.IconURL
if len(payload.Attachments) > 0 {
payload.Attachments[0].Color = slack.Color
} }
return s, nil return payload, nil
} }

4
modules/bindata/bindata.go

File diff suppressed because one or more lines are too long

1
modules/form/repo.go

@ -135,6 +135,7 @@ func (f *ProtectBranch) Validate(ctx *macaron.Context, errs binding.Errors) bind
type Webhook struct { type Webhook struct {
Events string Events string
Create bool Create bool
Delete bool
Push bool Push bool
PullRequest bool PullRequest bool
Active bool Active bool

1
routers/repo/webhook.go

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

10
templates/repo/settings/webhook_settings.tmpl

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

2
vendor/github.com/gogits/go-gogs-client/gogs.go generated vendored

@ -14,7 +14,7 @@ import (
) )
func Version() string { func Version() string {
return "0.12.6" return "0.12.7"
} }
// Client represents a Gogs API client. // Client represents a Gogs API client.

27
vendor/github.com/gogits/go-gogs-client/repo_hook.go generated vendored

@ -91,6 +91,7 @@ type PayloadCommit struct {
var ( var (
_ Payloader = &CreatePayload{} _ Payloader = &CreatePayload{}
_ Payloader = &DeletePayload{}
_ Payloader = &PushPayload{} _ Payloader = &PushPayload{}
_ Payloader = &PullRequestPayload{} _ Payloader = &PullRequestPayload{}
) )
@ -105,6 +106,7 @@ var (
type CreatePayload struct { type CreatePayload struct {
Ref string `json:"ref"` Ref string `json:"ref"`
RefType string `json:"ref_type"` RefType string `json:"ref_type"`
DefaultBranch string `json:"default_branch"`
Repo *Repository `json:"repository"` Repo *Repository `json:"repository"`
Sender *User `json:"sender"` Sender *User `json:"sender"`
} }
@ -133,6 +135,31 @@ func ParseCreateHook(raw []byte) (*CreatePayload, error) {
return hook, nil return hook, nil
} }
// ________ .__ __
// \______ \ ____ | | _____/ |_ ____
// | | \_/ __ \| | _/ __ \ __\/ __ \
// | ` \ ___/| |_\ ___/| | \ ___/
// /_______ /\___ >____/\___ >__| \___ >
// \/ \/ \/ \/
type PusherType string
const (
PUSHER_TYPE_USER PusherType = "user"
)
type DeletePayload struct {
Ref string `json:"ref"`
RefType string `json:"ref_type"`
PusherType PusherType `json:"pusher_type"`
Repo *Repository `json:"repository"`
Sender *User `json:"sender"`
}
func (p *DeletePayload) JSONPayload() ([]byte, error) {
return json.MarshalIndent(p, "", " ")
}
// __________ .__ // __________ .__
// \______ \__ __ _____| |__ // \______ \__ __ _____| |__
// | ___/ | \/ ___/ | \ // | ___/ | \/ ___/ | \

6
vendor/vendor.json vendored

@ -165,10 +165,10 @@
"revisionTime": "2017-02-19T18:16:29Z" "revisionTime": "2017-02-19T18:16:29Z"
}, },
{ {
"checksumSHA1": "exKX51W/Hieq7OOmYK2gYn+Huuw=", "checksumSHA1": "rtJ+nZ9VHh2X2Zon7wLczPAAc/s=",
"path": "github.com/gogits/go-gogs-client", "path": "github.com/gogits/go-gogs-client",
"revision": "f12fbacb5495120dc62dae7cfdf140d39bf6f715", "revision": "ba630f557c8349952183305373fa89b155202bac",
"revisionTime": "2017-02-24T06:16:35Z" "revisionTime": "2017-02-24T20:25:47Z"
}, },
{ {
"checksumSHA1": "p4yoFWgDiTfpu1JYgh26t6+VDTk=", "checksumSHA1": "p4yoFWgDiTfpu1JYgh26t6+VDTk=",

Loading…
Cancel
Save