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<PushCertificate>() {
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 hasNext();
223 PushCertificate n = next;
224 if (n == null) {
225 throw new NoSuchElementException();
226 }
227 next = null;
228 return n;
229 }
230
231 @Override
232 public void remove() {
233 throw new UnsupportedOperationException();
234 }
235 };
236 }
237
238 void load() throws IOException {
239 close();
240 reader = db.newObjectReader();
241 Ref ref = db.getRefDatabase().exactRef(REF_NAME);
242 if (ref == null) {
243
244 return;
245 }
246 try (RevWalk rw = new RevWalk(reader)) {
247 commit = rw.parseCommit(ref.getObjectId());
248 }
249 }
250
251 static PushCertificate read(TreeWalk tw) throws IOException {
252 if (tw == null || (tw.getRawMode(0) & TYPE_FILE) != TYPE_FILE) {
253 return null;
254 }
255 ObjectLoader loader =
256 tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB);
257 try (InputStream in = loader.openStream();
258 Reader r = new BufferedReader(
259 new InputStreamReader(in, UTF_8))) {
260 return PushCertificateParser.fromReader(r);
261 }
262 }
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282 public void put(PushCertificate cert, PersonIdent ident) {
283 put(cert, ident, null);
284 }
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308 public void put(PushCertificate cert, PersonIdent ident,
309 Collection<ReceiveCommand> matching) {
310 pending.add(new PendingCert(cert, ident, matching));
311 }
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328 public RefUpdate.Result save() throws IOException {
329 ObjectId newId = write();
330 if (newId == null) {
331 return RefUpdate.Result.NO_CHANGE;
332 }
333 try (ObjectInserter inserter = db.newObjectInserter()) {
334 RefUpdate.Result result = updateRef(newId);
335 switch (result) {
336 case FAST_FORWARD:
337 case NEW:
338 case NO_CHANGE:
339 pending.clear();
340 break;
341 default:
342 break;
343 }
344 return result;
345 } finally {
346 close();
347 }
348 }
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369 public boolean save(BatchRefUpdate batch) throws IOException {
370 ObjectId newId = write();
371 if (newId == null || newId.equals(commit)) {
372 return false;
373 }
374 batch.addCommand(new ReceiveCommand(
375 commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
376 return true;
377 }
378
379
380
381
382
383 public void clear() {
384 pending.clear();
385 }
386
387 private ObjectId write() throws IOException {
388 if (pending.isEmpty()) {
389 return null;
390 }
391 if (reader == null) {
392 load();
393 }
394 sortPending(pending);
395
396 ObjectId curr = commit;
397 DirCache dc = newDirCache();
398 try (ObjectInserter inserter = db.newObjectInserter()) {
399 for (PendingCert pc : pending) {
400 curr = saveCert(inserter, dc, pc, curr);
401 }
402 inserter.flush();
403 return curr;
404 }
405 }
406
407 private static void sortPending(List<PendingCert> pending) {
408 Collections.sort(pending, (PendingCert a, PendingCert b) -> Long.signum(
409 a.ident.getWhen().getTime() - b.ident.getWhen().getTime()));
410 }
411
412 private DirCache newDirCache() throws IOException {
413 if (commit != null) {
414 return DirCache.read(reader, commit.getTree());
415 }
416 return DirCache.newInCore();
417 }
418
419 private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
420 PendingCert pc, ObjectId curr) throws IOException {
421 Map<String, ReceiveCommand> byRef;
422 if (pc.matching != null) {
423 byRef = new HashMap<>();
424 for (ReceiveCommand cmd : pc.matching) {
425 if (byRef.put(cmd.getRefName(), cmd) != null) {
426 throw new IllegalStateException();
427 }
428 }
429 } else {
430 byRef = null;
431 }
432
433 DirCacheEditor editor = dc.editor();
434 String certText = pc.cert.toText() + pc.cert.getSignature();
435 final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
436 boolean any = false;
437 for (ReceiveCommand cmd : pc.cert.getCommands()) {
438 if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
439 continue;
440 }
441 any = true;
442 editor.add(new PathEdit(pathName(cmd.getRefName())) {
443 @Override
444 public void apply(DirCacheEntry ent) {
445 ent.setFileMode(FileMode.REGULAR_FILE);
446 ent.setObjectId(certId);
447 }
448 });
449 }
450 if (!any) {
451 return curr;
452 }
453 editor.finish();
454 CommitBuilder cb = new CommitBuilder();
455 cb.setAuthor(pc.ident);
456 cb.setCommitter(pc.ident);
457 cb.setTreeId(dc.writeTree(inserter));
458 if (curr != null) {
459 cb.setParentId(curr);
460 } else {
461 cb.setParentIds(Collections.<ObjectId> emptyList());
462 }
463 cb.setMessage(buildMessage(pc.cert));
464 return inserter.insert(OBJ_COMMIT, cb.build());
465 }
466
467 private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
468 if (c1 == null || c2 == null) {
469 return c1 == c2;
470 }
471 return c1.getRefName().equals(c2.getRefName())
472 && c1.getOldId().equals(c2.getOldId())
473 && c1.getNewId().equals(c2.getNewId());
474 }
475
476 private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
477 RefUpdate ru = db.updateRef(REF_NAME);
478 ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
479 ru.setNewObjectId(newId);
480 ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
481 ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
482 try (RevWalk rw = new RevWalk(reader)) {
483 return ru.update(rw);
484 }
485 }
486
487 private TreeWalk newTreeWalk(String refName) throws IOException {
488 if (commit == null) {
489 return null;
490 }
491 return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
492 }
493
494 static String pathName(String refName) {
495 return refName + "@{cert}";
496 }
497
498 private static String buildMessage(PushCertificate cert) {
499 StringBuilder sb = new StringBuilder();
500 if (cert.getCommands().size() == 1) {
501 sb.append(MessageFormat.format(
502 JGitText.get().storePushCertOneRef,
503 cert.getCommands().get(0).getRefName()));
504 } else {
505 sb.append(MessageFormat.format(
506 JGitText.get().storePushCertMultipleRefs,
507 Integer.valueOf(cert.getCommands().size())));
508 }
509 return sb.append('\n').toString();
510 }
511 }