internal/httpd/atom.go
195 lines · 6047 bytes
12 symbols in this file
1package httpd
2
3import (
4 "encoding/json"
5 "encoding/xml"
6 "fmt"
7 "net/http"
8 "strings"
9 "time"
10
11 "gitbay.org/gitbay/internal/gitutil"
12 "gitbay.org/gitbay/internal/sig"
13 "gitbay.org/gitbay/internal/store"
14)
15
16// Atom feeds (#192): a repository's releases and commits, and an owner's
17// public activity. Renderers over rows other surfaces already serve;
18// nothing is stored for them. A feed reader carries no session, so the
19// owner feed covers public repositories only, and a private repository
20// answers 404 as its pages do.
21
22type atomFeed struct {
23 XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
24 ID string `xml:"id"`
25 Title string `xml:"title"`
26 Updated string `xml:"updated"`
27 Links []atomLink `xml:"link"`
28 Entries []atomEntry `xml:"entry"`
29}
30
31type atomLink struct {
32 Rel string `xml:"rel,attr"`
33 Href string `xml:"href,attr"`
34 Type string `xml:"type,attr,omitempty"`
35}
36
37type atomEntry struct {
38 ID string `xml:"id"`
39 Title string `xml:"title"`
40 Updated string `xml:"updated"`
41 Author *atomAuthor `xml:"author,omitempty"`
42 Link atomLink `xml:"link"`
43 Content *atomContent `xml:"content,omitempty"`
44}
45
46type atomAuthor struct {
47 Name string `xml:"name"`
48}
49
50type atomContent struct {
51 Type string `xml:"type,attr"`
52 Text string `xml:",chardata"`
53}
54
55const atomLimit = 50
56
57func (s *Server) site() string { return strings.TrimSuffix(s.cfg.Server.SiteURL, "/") }
58
59// writeAtom serialises the feed. Updated falls back to the newest entry,
60// then to now: a feed with no entries is still a feed.
61func (s *Server) writeAtom(w http.ResponseWriter, path, title string, entries []atomEntry) {
62 f := atomFeed{ID: s.site() + path, Title: title, Entries: entries,
63 Links: []atomLink{
64 {Rel: "self", Href: s.site() + path, Type: "application/atom+xml"},
65 {Rel: "alternate", Href: s.site() + strings.TrimSuffix(strings.TrimSuffix(path, ".atom"), "/activity")},
66 }}
67 if len(entries) > 0 {
68 f.Updated = entries[0].Updated
69 } else {
70 f.Updated = time.Now().UTC().Format(time.RFC3339)
71 }
72 w.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
73 w.Header().Set("X-Content-Type-Options", "nosniff")
74 fmt.Fprint(w, xml.Header)
75 xml.NewEncoder(w).Encode(f)
76}
77
78func (s *Server) releasesAtom(w http.ResponseWriter, r *http.Request) {
79 p, ok := s.repoFor(w, r, "")
80 if !ok {
81 return
82 }
83 rels, err := s.st.ListReleases(p.Repo.ID)
84 if err != nil {
85 http.Error(w, "internal error", http.StatusInternalServerError)
86 return
87 }
88 base := "/" + p.Repo.Path()
89 var entries []atomEntry
90 for _, rel := range rels {
91 title := rel.Tag
92 if rel.Title != "" {
93 title = rel.Tag + ": " + rel.Title
94 }
95 e := atomEntry{ID: s.site() + base + "/releases#" + rel.Tag, Title: title, Updated: rel.CreatedAt,
96 Link: atomLink{Rel: "alternate", Href: s.site() + base + "/releases#" + rel.Tag}}
97 if rel.Author != "" {
98 e.Author = &atomAuthor{Name: rel.Author}
99 }
100 if rel.Notes != "" {
101 e.Content = &atomContent{Type: "text", Text: rel.Notes}
102 }
103 entries = append(entries, e)
104 }
105 s.writeAtom(w, base+"/releases.atom", p.Repo.Path()+" releases", entries)
106}
107
108func (s *Server) logAtom(w http.ResponseWriter, r *http.Request) {
109 p, ok := s.repoFor(w, r, r.PathValue("ref"))
110 if !ok {
111 return
112 }
113 shas, err := gitutil.RevList(p.Dir, p.Ref, atomLimit)
114 if err != nil {
115 s.notFound(w, r)
116 return
117 }
118 base := "/" + p.Repo.Path()
119 var entries []atomEntry
120 for _, sha := range shas {
121 e := atomEntry{ID: s.site() + base + "/commit/" + sha, Title: sha[:10],
122 Link: atomLink{Rel: "alternate", Href: s.site() + base + "/commit/" + sha}}
123 if raw, err := gitutil.ReadCommit(p.Dir, sha); err == nil {
124 if c, err := sig.ParseCommit(raw); err == nil {
125 e.Title = c.Subject
126 e.Updated = time.Unix(c.AuthorUnix, 0).UTC().Format(time.RFC3339)
127 e.Author = &atomAuthor{Name: c.AuthorName}
128 }
129 }
130 entries = append(entries, e)
131 }
132 path := base + "/log.atom"
133 if r.PathValue("ref") != "" {
134 path += "/" + p.Ref
135 }
136 s.writeAtom(w, path, p.Repo.Path()+" commits on "+p.Ref, entries)
137}
138
139// ownerAtom is an owner's activity on their public repositories, the
140// feed command's rows without a viewer.
141func (s *Server) ownerAtom(w http.ResponseWriter, r *http.Request) {
142 name := r.PathValue("owner")
143 kind, id := "", int64(0)
144 if u, err := s.st.UserByUsername(name); err == nil {
145 kind, id = "user", u.ID
146 } else if o, err := s.st.OrgByName(name); err == nil {
147 kind, id = "org", o.ID
148 } else {
149 s.notFound(w, r)
150 return
151 }
152 events, err := s.st.OwnerPublicEvents(kind, id, atomLimit)
153 if err != nil {
154 http.Error(w, "internal error", http.StatusInternalServerError)
155 return
156 }
157 var entries []atomEntry
158 for _, ev := range events {
159 title, link := eventTitle(ev)
160 e := atomEntry{ID: fmt.Sprintf("%s/%s/activity.atom#%d", s.site(), name, ev.ID), Title: title, Updated: ev.CreatedAt,
161 Link: atomLink{Rel: "alternate", Href: s.site() + link}}
162 if ev.Actor != "" {
163 e.Author = &atomAuthor{Name: ev.Actor}
164 }
165 entries = append(entries, e)
166 }
167 s.writeAtom(w, "/"+name+"/activity.atom", name+" activity", entries)
168}
169
170// eventTitle names an event and where it points, from the kind and the
171// number or tag its data carries.
172func eventTitle(ev store.FeedEvent) (title, link string) {
173 var data struct {
174 Number int64 `json:"number"`
175 Tag string `json:"tag"`
176 }
177 json.Unmarshal([]byte(ev.Data), &data)
178 link = "/" + ev.RepoPath
179 title = ev.Kind + " in " + ev.RepoPath
180 switch {
181 case strings.HasPrefix(ev.Kind, "issue.") && data.Number > 0:
182 link += fmt.Sprintf("/issues/%d", data.Number)
183 title = fmt.Sprintf("%s %s#%d", ev.Kind, ev.RepoPath, data.Number)
184 case strings.HasPrefix(ev.Kind, "mr.") && data.Number > 0:
185 link += fmt.Sprintf("/mrs/%d", data.Number)
186 title = fmt.Sprintf("%s %s!%d", ev.Kind, ev.RepoPath, data.Number)
187 case strings.HasPrefix(ev.Kind, "release.") && data.Tag != "":
188 link += "/releases#" + data.Tag
189 title = fmt.Sprintf("%s %s %s", ev.Kind, ev.RepoPath, data.Tag)
190 }
191 if ev.Actor != "" {
192 title = ev.Actor + ": " + title
193 }
194 return title, link
195}