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