1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44 package org.eclipse.jgit.transport;
45
46 import static java.nio.charset.StandardCharsets.UTF_8;
47 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
48 import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
49 import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;
50
51 import java.io.BufferedReader;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.io.InputStreamReader;
55 import java.io.Reader;
56 import java.text.MessageFormat;
57 import java.util.ArrayList;
58 import java.util.Collection;
59 import java.util.Collections;
60 import java.util.Comparator;
61 import java.util.HashMap;
62 import java.util.Iterator;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.NoSuchElementException;
66
67 import org.eclipse.jgit.dircache.DirCache;
68 import org.eclipse.jgit.dircache.DirCacheBuilder;
69 import org.eclipse.jgit.dircache.DirCacheEditor;
70 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
71 import org.eclipse.jgit.dircache.DirCacheEntry;
72 import org.eclipse.jgit.internal.JGitText;
73 import org.eclipse.jgit.lib.BatchRefUpdate;
74 import org.eclipse.jgit.lib.CommitBuilder;
75 import org.eclipse.jgit.lib.Constants;
76 import org.eclipse.jgit.lib.FileMode;
77 import org.eclipse.jgit.lib.ObjectId;
78 import org.eclipse.jgit.lib.ObjectInserter;
79 import org.eclipse.jgit.lib.ObjectLoader;
80 import org.eclipse.jgit.lib.ObjectReader;
81 import org.eclipse.jgit.lib.PersonIdent;
82 import org.eclipse.jgit.lib.Ref;
83 import org.eclipse.jgit.lib.RefUpdate;
84 import org.eclipse.jgit.lib.Repository;
85 import org.eclipse.jgit.revwalk.RevCommit;
86 import org.eclipse.jgit.revwalk.RevWalk;
87 import org.eclipse.jgit.treewalk.TreeWalk;
88 import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
89 import org.eclipse.jgit.treewalk.filter.PathFilter;
90 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
91 import org.eclipse.jgit.treewalk.filter.TreeFilter;
92
93
94
95
96
97
98
99
100
101
102
103
104 public class PushCertificateStore implements AutoCloseable {
105
106 static final String REF_NAME =
107 Constants.R_REFS + "meta/push-certs";
108
109 private static class PendingCert {
110 private PushCertificate cert;
111 private PersonIdent ident;
112 private Collection<ReceiveCommand> matching;
113
114 private PendingCert(PushCertificate cert, PersonIdent ident,
115 Collection<ReceiveCommand> matching) {
116 this.cert = cert;
117 this.ident = ident;
118 this.matching = matching;
119 }
120 }
121
122 private final Repository db;
123 private final List<PendingCert> pending;
124 private ObjectReader reader;
125 private RevCommit commit;
126
127
128
129
130
131
132
133 public PushCertificateStore(Repository db) {
134 this.db = db;
135 pending = new ArrayList<>();
136 }
137
138
139
140
141
142
143
144 public void close() {
145 if (reader != null) {
146 reader.close();
147 reader = null;
148 commit = null;
149 }
150 }
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166 public PushCertificate get(String refName) throws IOException {
167 if (reader == null) {
168 load();
169 }
170 try (TreeWalk tw = newTreeWalk(refName)) {
171 return read(tw);
172 }
173 }
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193 public Iterable<PushCertificate> getAll(final String refName) {
194 return new Iterable<PushCertificate>() {
195 @Override
196 public Iterator<PushCertificate> iterator() {
197 return new Iterator<PushCertificate>() {
198 private final String path = pathName(refName);
199 private PushCertificate next;
200
201 private RevWalk rw;
202 {
203 try {
204 if (reader == null) {
205 load();
206 }
207 if (commit != null) {
208 rw = new RevWalk(reader);
209 rw.setTreeFilter(AndTreeFilter.create(
210 PathFilterGroup.create(
211 Collections.singleton(PathFilter.create(path))),
212 TreeFilter.ANY_DIFF));
213 rw.setRewriteParents(false);
214 rw.markStart(rw.parseCommit(commit));
215 } else {
216 rw = null;
217 }
218 } catch (IOException e) {
219 throw new RuntimeException(e);
220 }
221 }
222
223 @Override
224 public boolean hasNext() {
225 try {
226 if (next == null) {
227 if (rw == null) {
228 return false;
229 }
230 try {
231 RevCommit c = rw.next();
232 if (c != null) {
233 try (TreeWalk tw = TreeWalk.forPath(
234 rw.getObjectReader(), path, c.getTree())) {
235 next = read(tw);
236 }
237 } else {
238 next = null;
239 }
240 } catch (IOException e) {
241 throw new RuntimeException(e);
242 }
243 }
244 return next != null;
245 } finally {
246 if (next == null && rw != null) {
247 rw.close();
248 rw = null;
249 }
250 }
251 }
252
253 @Override
254 public PushCertificate next() {
255 hasNext();
256 PushCertificate n = next;
257 if (n == null) {
258 throw new NoSuchElementException();
259 }
260 next = null;
261 return n;
262 }
263
264 @Override
265 public void remove() {
266 throw new UnsupportedOperationException();
267 }
268 };
269 }
270 };
271 }
272
273 private void load() throws IOException {
274 close();
275 reader = db.newObjectReader();
276 Ref ref = db.getRefDatabase().exactRef(REF_NAME);
277 if (ref == null) {
278
279 return;
280 }
281 try (RevWalk rw = new RevWalk(reader)) {
282 commit = rw.parseCommit(ref.getObjectId());
283 }
284 }
285
286 private static PushCertificate read(TreeWalk tw) throws IOException {
287 if (tw == null || (tw.getRawMode(0) & TYPE_FILE) != TYPE_FILE) {
288 return null;
289 }
290 ObjectLoader loader =
291 tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB);
292 try (InputStream in = loader.openStream();
293 Reader r = new BufferedReader(new InputStreamReader(in, UTF_8))) {
294 return PushCertificateParser.fromReader(r);
295 }
296 }
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316 public void put(PushCertificate cert, PersonIdent ident) {
317 put(cert, ident, null);
318 }
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342 public void put(PushCertificate cert, PersonIdent ident,
343 Collection<ReceiveCommand> matching) {
344 pending.add(new PendingCert(cert, ident, matching));
345 }
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362 public RefUpdate.Result save() throws IOException {
363 ObjectId newId = write();
364 if (newId == null) {
365 return RefUpdate.Result.NO_CHANGE;
366 }
367 try (ObjectInserter inserter = db.newObjectInserter()) {
368 RefUpdate.Result result = updateRef(newId);
369 switch (result) {
370 case FAST_FORWARD:
371 case NEW:
372 case NO_CHANGE:
373 pending.clear();
374 break;
375 default:
376 break;
377 }
378 return result;
379 } finally {
380 close();
381 }
382 }
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402 public boolean save(BatchRefUpdate batch) throws IOException {
403 ObjectId newId = write();
404 if (newId == null || newId.equals(commit)) {
405 return false;
406 }
407 batch.addCommand(new ReceiveCommand(
408 commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
409 return true;
410 }
411
412
413
414
415
416 public void clear() {
417 pending.clear();
418 }
419
420 private ObjectId write() throws IOException {
421 if (pending.isEmpty()) {
422 return null;
423 }
424 if (reader == null) {
425 load();
426 }
427 sortPending(pending);
428
429 ObjectId curr = commit;
430 DirCache dc = newDirCache();
431 try (ObjectInserter inserter = db.newObjectInserter()) {
432 for (PendingCert pc : pending) {
433 curr = saveCert(inserter, dc, pc, curr);
434 }
435 inserter.flush();
436 return curr;
437 }
438 }
439
440 private static void sortPending(List<PendingCert> pending) {
441 Collections.sort(pending, new Comparator<PendingCert>() {
442 @Override
443 public int compare(PendingCert a, PendingCert b) {
444 return Long.signum(
445 a.ident.getWhen().getTime() - b.ident.getWhen().getTime());
446 }
447 });
448 }
449
450 private DirCache newDirCache() throws IOException {
451 DirCache dc = DirCache.newInCore();
452 if (commit != null) {
453 DirCacheBuilder b = dc.builder();
454 b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, commit.getTree());
455 b.finish();
456 }
457 return dc;
458 }
459
460 private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
461 PendingCert pc, ObjectId curr) throws IOException {
462 Map<String, ReceiveCommand> byRef;
463 if (pc.matching != null) {
464 byRef = new HashMap<>();
465 for (ReceiveCommand cmd : pc.matching) {
466 if (byRef.put(cmd.getRefName(), cmd) != null) {
467 throw new IllegalStateException();
468 }
469 }
470 } else {
471 byRef = null;
472 }
473
474 DirCacheEditor editor = dc.editor();
475 String certText = pc.cert.toText() + pc.cert.getSignature();
476 final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
477 boolean any = false;
478 for (ReceiveCommand cmd : pc.cert.getCommands()) {
479 if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
480 continue;
481 }
482 any = true;
483 editor.add(new PathEdit(pathName(cmd.getRefName())) {
484 @Override
485 public void apply(DirCacheEntry ent) {
486 ent.setFileMode(FileMode.REGULAR_FILE);
487 ent.setObjectId(certId);
488 }
489 });
490 }
491 if (!any) {
492 return curr;
493 }
494 editor.finish();
495 CommitBuilder cb = new CommitBuilder();
496 cb.setAuthor(pc.ident);
497 cb.setCommitter(pc.ident);
498 cb.setTreeId(dc.writeTree(inserter));
499 if (curr != null) {
500 cb.setParentId(curr);
501 } else {
502 cb.setParentIds(Collections.<ObjectId> emptyList());
503 }
504 cb.setMessage(buildMessage(pc.cert));
505 return inserter.insert(OBJ_COMMIT, cb.build());
506 }
507
508 private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
509 if (c1 == null || c2 == null) {
510 return c1 == c2;
511 }
512 return c1.getRefName().equals(c2.getRefName())
513 && c1.getOldId().equals(c2.getOldId())
514 && c1.getNewId().equals(c2.getNewId());
515 }
516
517 private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
518 RefUpdate ru = db.updateRef(REF_NAME);
519 ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
520 ru.setNewObjectId(newId);
521 ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
522 ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
523 try (RevWalk rw = new RevWalk(reader)) {
524 return ru.update(rw);
525 }
526 }
527
528 private TreeWalk newTreeWalk(String refName) throws IOException {
529 if (commit == null) {
530 return null;
531 }
532 return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
533 }
534
535 private static String pathName(String refName) {
536 return refName + "@{cert}";
537 }
538
539 private static String buildMessage(PushCertificate cert) {
540 StringBuilder sb = new StringBuilder();
541 if (cert.getCommands().size() == 1) {
542 sb.append(MessageFormat.format(
543 JGitText.get().storePushCertOneRef,
544 cert.getCommands().get(0).getRefName()));
545 } else {
546 sb.append(MessageFormat.format(
547 JGitText.get().storePushCertMultipleRefs,
548 Integer.valueOf(cert.getCommands().size())));
549 }
550 return sb.append('\n').toString();
551 }
552 }