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