View Javadoc
1   /*
2    * Copyright (C) 2015, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
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   * Storage for recorded push certificates.
60   * <p>
61   * Push certificates are stored in a special ref {@code refs/meta/push-certs}.
62   * The filenames in the tree are ref names followed by the special suffix
63   * <code>@{cert}</code>, and the contents are the latest push cert affecting
64   * that ref. The special suffix allows storing certificates for both refs/foo
65   * and refs/foo/bar in case those both existed at some point.
66   *
67   * @since 4.1
68   */
69  public class PushCertificateStore implements AutoCloseable {
70  	/** Ref name storing push certificates. */
71  	static final String REF_NAME =
72  			Constants.R_REFS + "meta/push-certs"; //$NON-NLS-1$
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  	 * Create a new store backed by the given repository.
94  	 *
95  	 * @param db
96  	 *            the repository.
97  	 */
98  	public PushCertificateStore(Repository db) {
99  		this.db = db;
100 		pending = new ArrayList<>();
101 	}
102 
103 	/**
104 	 * {@inheritDoc}
105 	 * <p>
106 	 * Close resources opened by this store.
107 	 * <p>
108 	 * If {@link #get(String)} was called, closes the cached object reader
109 	 * created by that method. Does not close the underlying repository.
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 	 * Get latest push certificate associated with a ref.
122 	 * <p>
123 	 * Lazily opens {@code refs/meta/push-certs} and reads from the repository as
124 	 * necessary. The state is cached between calls to {@code get}; to reread the,
125 	 * call {@link #close()} first.
126 	 *
127 	 * @param refName
128 	 *            the ref name to get the certificate for.
129 	 * @return last certificate affecting the ref, or null if no cert was recorded
130 	 *         for the last update to this ref.
131 	 * @throws java.io.IOException
132 	 *             if a problem occurred reading the repository.
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 	 * Iterate over all push certificates affecting a ref.
145 	 * <p>
146 	 * Only includes push certificates actually stored in the tree; see class
147 	 * Javadoc for conditions where this might not include all push certs ever
148 	 * seen for this ref.
149 	 * <p>
150 	 * The returned iterable may be iterated multiple times, and push certs will
151 	 * be re-read from the current state of the store on each call to {@link
152 	 * Iterable#iterator()}. However, method calls on the returned iterator may
153 	 * fail if {@code save} or {@code close} is called on the enclosing store
154 	 * during iteration.
155 	 *
156 	 * @param refName
157 	 *            the ref name to get certificates for.
158 	 * @return iterable over certificates; must be fully iterated in order to
159 	 *         close resources.
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 			// No ref, same as empty.
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 	 * Put a certificate to be saved to the store.
266 	 * <p>
267 	 * Writes the contents of this certificate for each ref mentioned. It is up
268 	 * to the caller to ensure this certificate accurately represents the state
269 	 * of the ref.
270 	 * <p>
271 	 * Pending certificates added to this method are not returned by
272 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
273 	 * {@link #save()}.
274 	 *
275 	 * @param cert
276 	 *            certificate to store.
277 	 * @param ident
278 	 *            identity for the commit that stores this certificate. Pending
279 	 *            certificates are sorted by identity timestamp during
280 	 *            {@link #save()}.
281 	 */
282 	public void put(PushCertificate cert, PersonIdent ident) {
283 		put(cert, ident, null);
284 	}
285 
286 	/**
287 	 * Put a certificate to be saved to the store, matching a set of commands.
288 	 * <p>
289 	 * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
290 	 * stored for a push certificate if there is a corresponding command in the
291 	 * list that exactly matches the old/new values mentioned in the push
292 	 * certificate.
293 	 * <p>
294 	 * Pending certificates added to this method are not returned by
295 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
296 	 * {@link #save()}.
297 	 *
298 	 * @param cert
299 	 *            certificate to store.
300 	 * @param ident
301 	 *            identity for the commit that stores this certificate. Pending
302 	 *            certificates are sorted by identity timestamp during
303 	 *            {@link #save()}.
304 	 * @param matching
305 	 *            only store certs for the refs listed in this list whose values
306 	 *            match the commands in the cert.
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 	 * Save pending certificates to the store.
315 	 * <p>
316 	 * One commit is created per certificate added with
317 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
318 	 * timestamps, and a single ref update is performed.
319 	 * <p>
320 	 * The pending list is cleared if and only the ref update fails, which
321 	 * allows for easy retries in case of lock failure.
322 	 *
323 	 * @return the result of attempting to update the ref.
324 	 * @throws java.io.IOException
325 	 *             if there was an error reading from or writing to the
326 	 *             repository.
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 	 * Save pending certificates to the store in an existing batch ref update.
352 	 * <p>
353 	 * One commit is created per certificate added with
354 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
355 	 * timestamps, all commits are flushed, and a single command is added to the
356 	 * batch.
357 	 * <p>
358 	 * The cached ref value and pending list are <em>not</em> cleared. If the
359 	 * ref update succeeds, the caller is responsible for calling
360 	 * {@link #close()} and/or {@link #clear()}.
361 	 *
362 	 * @param batch
363 	 *            update to save to.
364 	 * @return whether a command was added to the batch.
365 	 * @throws java.io.IOException
366 	 *             if there was an error reading from or writing to the
367 	 *             repository.
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 	 * Clear pending certificates added with {@link #put(PushCertificate,
381 	 * PersonIdent)}.
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}"; //$NON-NLS-1$
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 }