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<>() {
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 			// No ref, same as empty.
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 	 * Put a certificate to be saved to the store.
265 	 * <p>
266 	 * Writes the contents of this certificate for each ref mentioned. It is up
267 	 * to the caller to ensure this certificate accurately represents the state
268 	 * of the ref.
269 	 * <p>
270 	 * Pending certificates added to this method are not returned by
271 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
272 	 * {@link #save()}.
273 	 *
274 	 * @param cert
275 	 *            certificate to store.
276 	 * @param ident
277 	 *            identity for the commit that stores this certificate. Pending
278 	 *            certificates are sorted by identity timestamp during
279 	 *            {@link #save()}.
280 	 */
281 	public void put(PushCertificate cert, PersonIdent ident) {
282 		put(cert, ident, null);
283 	}
284 
285 	/**
286 	 * Put a certificate to be saved to the store, matching a set of commands.
287 	 * <p>
288 	 * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
289 	 * stored for a push certificate if there is a corresponding command in the
290 	 * list that exactly matches the old/new values mentioned in the push
291 	 * certificate.
292 	 * <p>
293 	 * Pending certificates added to this method are not returned by
294 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
295 	 * {@link #save()}.
296 	 *
297 	 * @param cert
298 	 *            certificate to store.
299 	 * @param ident
300 	 *            identity for the commit that stores this certificate. Pending
301 	 *            certificates are sorted by identity timestamp during
302 	 *            {@link #save()}.
303 	 * @param matching
304 	 *            only store certs for the refs listed in this list whose values
305 	 *            match the commands in the cert.
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 	 * Save pending certificates to the store.
314 	 * <p>
315 	 * One commit is created per certificate added with
316 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
317 	 * timestamps, and a single ref update is performed.
318 	 * <p>
319 	 * The pending list is cleared if and only the ref update fails, which
320 	 * allows for easy retries in case of lock failure.
321 	 *
322 	 * @return the result of attempting to update the ref.
323 	 * @throws java.io.IOException
324 	 *             if there was an error reading from or writing to the
325 	 *             repository.
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 	 * Save pending certificates to the store in an existing batch ref update.
351 	 * <p>
352 	 * One commit is created per certificate added with
353 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
354 	 * timestamps, all commits are flushed, and a single command is added to the
355 	 * batch.
356 	 * <p>
357 	 * The cached ref value and pending list are <em>not</em> cleared. If the
358 	 * ref update succeeds, the caller is responsible for calling
359 	 * {@link #close()} and/or {@link #clear()}.
360 	 *
361 	 * @param batch
362 	 *            update to save to.
363 	 * @return whether a command was added to the batch.
364 	 * @throws java.io.IOException
365 	 *             if there was an error reading from or writing to the
366 	 *             repository.
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 	 * Clear pending certificates added with {@link #put(PushCertificate,
380 	 * PersonIdent)}.
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}"; //$NON-NLS-1$
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 }