blob: fad20295451637766018db1e5343013c85ac076c [file] [log] [blame]
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +02001package service
2
3import (
4 "context"
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +02005 "fmt"
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +02006 "io"
7 "regexp"
8 "strings"
9 "sync"
10 "time"
11
12 "github.com/golang/glog"
13 "google.golang.org/grpc/codes"
14 "google.golang.org/grpc/status"
15
16 git "github.com/go-git/go-git/v5"
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020017 "github.com/go-git/go-git/v5/config"
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020018 "github.com/go-git/go-git/v5/plumbing"
19 "github.com/go-git/go-git/v5/plumbing/filemode"
20 "github.com/go-git/go-git/v5/plumbing/object"
21 "github.com/go-git/go-git/v5/storage"
22 "github.com/go-git/go-git/v5/storage/memory"
23
24 pb "code.hackerspace.pl/hscloud/devtools/depotview/proto"
25)
26
27var (
28 reHash = regexp.MustCompile(`[a-f0-9]{40,64}`)
29)
30
31type Service struct {
32 remote string
33 storer storage.Storer
34
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020035 mu sync.Mutex
36 repo *git.Repository
37 lastFetch time.Time
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020038}
39
40func New(remote string) *Service {
41 return &Service{
42 remote: remote,
43 storer: memory.NewStorage(),
44 }
45}
46
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020047func (s *Service) ensureRepo(ctx context.Context) error {
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020048 // Clone repository if necessary.
49 if s.repo == nil {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020050 repo, err := git.CloneContext(ctx, s.storer, nil, &git.CloneOptions{
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020051 URL: s.remote,
52 })
53 if err != nil {
54 glog.Errorf("Clone(%q): %v", s.remote, err)
55 return status.Error(codes.Unavailable, "could not clone repository")
56 }
57 s.repo = repo
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020058 }
59
60 // Fetch if necessary.
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020061 if time.Since(s.lastFetch) > 10*time.Second {
62 glog.Infof("Fetching...")
63 err := s.repo.FetchContext(ctx, &git.FetchOptions{
64 RefSpecs: []config.RefSpec{
65 config.RefSpec("+refs/heads/*:refs/remotes/origin/*"),
66 config.RefSpec("+refs/changes/*:refs/changes/*"),
67 },
68 Force: true,
69 })
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020070 if err != nil && err != git.NoErrAlreadyUpToDate {
71 glog.Errorf("Fetch(): %v", err)
72 } else {
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020073 s.lastFetch = time.Now()
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020074 }
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020075
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020076 }
77
78 return nil
79}
80
81func (s *Service) Resolve(ctx context.Context, req *pb.ResolveRequest) (*pb.ResolveResponse, error) {
82 s.mu.Lock()
83 defer s.mu.Unlock()
84
85 if req.Ref == "" {
86 return nil, status.Error(codes.InvalidArgument, "ref must be set")
87 }
88
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020089 if err := s.ensureRepo(ctx); err != nil {
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020090 return nil, err
91 }
92
93 h, err := s.repo.ResolveRevision(plumbing.Revision(req.Ref))
94 switch {
95 case err == plumbing.ErrReferenceNotFound:
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +020096 return &pb.ResolveResponse{Hash: "", LastChecked: s.lastFetch.UnixNano()}, nil
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +020097 case err != nil:
98 return nil, status.Errorf(codes.Unavailable, "git resolve error: %v", err)
99 default:
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200100 return &pb.ResolveResponse{Hash: h.String(), LastChecked: s.lastFetch.UnixNano()}, nil
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +0200101 }
102}
103
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200104func (s *Service) ResolveGerritChange(ctx context.Context, req *pb.ResolveGerritChangeRequest) (*pb.ResolveGerritChangeResponse, error) {
105 if err := s.ensureRepo(ctx); err != nil {
106 return nil, err
107 }
108
109 // I'm totally guessing this, from these examples:
110 // refs/changes/03/3/meta
111 // refs/changes/77/77/meta
112 // refs/changes/47/247/meta
113 // etc...
114 shard := fmt.Sprintf("%02d", req.Change%100)
115 metaRef := fmt.Sprintf("refs/changes/%s/%d/meta", shard, req.Change)
116
117 h, err := s.repo.ResolveRevision(plumbing.Revision(metaRef))
118 switch {
119 case err == plumbing.ErrReferenceNotFound:
120 return &pb.ResolveGerritChangeResponse{Hash: "", LastChecked: s.lastFetch.UnixNano()}, nil
121 case err != nil:
122 return nil, status.Errorf(codes.Unavailable, "git metadata resolve error: %v", err)
123 }
124
125 c, err := s.repo.CommitObject(*h)
126 if err != nil {
127 return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
128 }
129
130 var messages []string
131 for {
132 messages = append([]string{c.Message}, messages...)
133
134 if len(c.ParentHashes) != 1 {
135 break
136 }
137
138 c, err = s.repo.CommitObject(c.ParentHashes[0])
139 if err != nil {
140 return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
141 }
142 }
143
144 meta := parseGerritMetadata(messages)
145 if meta == nil {
146 return nil, status.Errorf(codes.Internal, "could not parse gerrit metadata for ref %q", metaRef)
147 }
148 return &pb.ResolveGerritChangeResponse{Hash: meta.commit, LastChecked: s.lastFetch.UnixNano()}, nil
149}
150
151func (s *Service) getFile(ctx context.Context, hash, path string, notFoundOkay bool) (*object.File, error) {
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +0200152 if !reHash.MatchString(hash) {
153 return nil, status.Error(codes.InvalidArgument, "hash must be valid full git hash string")
154 }
155 if path == "" {
156 return nil, status.Error(codes.InvalidArgument, "path must be set")
157 }
158
159 path = pathNormalize(path)
160 if path == "" {
161 return nil, status.Error(codes.InvalidArgument, "path must be a valid unix or depot-style path")
162 }
163
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200164 if err := s.ensureRepo(ctx); err != nil {
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +0200165 return nil, err
166 }
167
168 c, err := s.repo.CommitObject(plumbing.NewHash(hash))
169 switch {
170 case err == plumbing.ErrObjectNotFound:
171 return nil, status.Error(codes.NotFound, "hash not found")
172 case err != nil:
173 return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
174 }
175
176 file, err := c.File(path)
177 switch {
178 case err == object.ErrFileNotFound && !notFoundOkay:
179 return nil, status.Error(codes.NotFound, "file not found")
180 case err == object.ErrFileNotFound && notFoundOkay:
181 return nil, nil
182 case err != nil:
183 return nil, status.Errorf(codes.Unavailable, "git error: %v", err)
184 }
185
186 return file, nil
187}
188
189func (s *Service) Stat(ctx context.Context, req *pb.StatRequest) (*pb.StatResponse, error) {
190 s.mu.Lock()
191 defer s.mu.Unlock()
192
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200193 file, err := s.getFile(ctx, req.Hash, req.Path, true)
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +0200194 if err != nil {
195 return nil, err
196 }
197
198 if file == nil {
199 return &pb.StatResponse{Type: pb.StatResponse_TYPE_NOT_PRESENT}, nil
200 }
201
202 switch {
203 case file.Mode == filemode.Dir:
204 return &pb.StatResponse{Type: pb.StatResponse_TYPE_DIRECTORY}, nil
205 case file.Mode.IsFile():
206 return &pb.StatResponse{Type: pb.StatResponse_TYPE_FILE}, nil
207 default:
208 return nil, status.Errorf(codes.Unimplemented, "unknown file type %o", file.Mode)
209 }
210}
211
212func (s *Service) Read(req *pb.ReadRequest, srv pb.DepotView_ReadServer) error {
213 s.mu.Lock()
214 defer s.mu.Unlock()
215
Sergiusz Bazanskif157b4d2020-04-10 17:39:43 +0200216 ctx := srv.Context()
217
218 file, err := s.getFile(ctx, req.Hash, req.Path, false)
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +0200219 if err != nil {
220 return err
221 }
222
223 reader, err := file.Reader()
224 if err != nil {
225 return status.Errorf(codes.Unavailable, "file read error: %v", err)
226 }
227 defer reader.Close()
228
Sergiusz Bazanski4c0e9b52020-04-08 22:42:33 +0200229 for {
230 if ctx.Err() != nil {
231 return ctx.Err()
232 }
233
234 // 1 MB read
235 chunk := make([]byte, 16*1024)
236 n, err := reader.Read(chunk)
237 switch {
238 case err == io.EOF:
239 n = 0
240 case err != nil:
241 return status.Errorf(codes.Unavailable, "file read error: %v", err)
242 }
243
244 err = srv.Send(&pb.ReadResponse{Data: chunk[:n]})
245 if err != nil {
246 return err
247 }
248
249 if n == 0 {
250 break
251 }
252 }
253
254 return nil
255}
256
257func pathNormalize(path string) string {
258 leadingSlashes := 0
259 for _, c := range path {
260 if c != '/' {
261 break
262 }
263 leadingSlashes += 1
264 }
265
266 // Only foo/bar, /foo/bar, and //foo/bar paths allowed.
267 if leadingSlashes > 2 {
268 return ""
269 }
270 path = path[leadingSlashes:]
271
272 // No trailing slashes allowed.
273 if strings.HasSuffix(path, "/") {
274 return ""
275 }
276
277 return path
278}