View Javadoc
1   /*
2    * Copyright (C) 2015, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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   * Storage for recorded push certificates.
94   * <p>
95   * Push certificates are stored in a special ref {@code refs/meta/push-certs}.
96   * The filenames in the tree are ref names followed by the special suffix
97   * <code>@{cert}</code>, and the contents are the latest push cert affecting
98   * that ref. The special suffix allows storing certificates for both refs/foo
99   * and refs/foo/bar in case those both existed at some point.
100  *
101  * @since 4.1
102  */
103 public class PushCertificateStore implements AutoCloseable {
104 	/** Ref name storing push certificates. */
105 	static final String REF_NAME =
106 			Constants.R_REFS + "meta/push-certs"; //$NON-NLS-1$
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 	 * Create a new store backed by the given repository.
128 	 *
129 	 * @param db
130 	 *            the repository.
131 	 */
132 	public PushCertificateStore(Repository db) {
133 		this.db = db;
134 		pending = new ArrayList<>();
135 	}
136 
137 	/**
138 	 * {@inheritDoc}
139 	 * <p>
140 	 * Close resources opened by this store.
141 	 * <p>
142 	 * If {@link #get(String)} was called, closes the cached object reader
143 	 * created by that method. Does not close the underlying repository.
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 	 * Get latest push certificate associated with a ref.
156 	 * <p>
157 	 * Lazily opens {@code refs/meta/push-certs} and reads from the repository as
158 	 * necessary. The state is cached between calls to {@code get}; to reread the,
159 	 * call {@link #close()} first.
160 	 *
161 	 * @param refName
162 	 *            the ref name to get the certificate for.
163 	 * @return last certificate affecting the ref, or null if no cert was recorded
164 	 *         for the last update to this ref.
165 	 * @throws java.io.IOException
166 	 *             if a problem occurred reading the repository.
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 	 * Iterate over all push certificates affecting a ref.
179 	 * <p>
180 	 * Only includes push certificates actually stored in the tree; see class
181 	 * Javadoc for conditions where this might not include all push certs ever
182 	 * seen for this ref.
183 	 * <p>
184 	 * The returned iterable may be iterated multiple times, and push certs will
185 	 * be re-read from the current state of the store on each call to {@link
186 	 * Iterable#iterator()}. However, method calls on the returned iterator may
187 	 * fail if {@code save} or {@code close} is called on the enclosing store
188 	 * during iteration.
189 	 *
190 	 * @param refName
191 	 *            the ref name to get certificates for.
192 	 * @return iterable over certificates; must be fully iterated in order to
193 	 *         close resources.
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 			// No ref, same as empty.
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 	 * Put a certificate to be saved to the store.
303 	 * <p>
304 	 * Writes the contents of this certificate for each ref mentioned. It is up
305 	 * to the caller to ensure this certificate accurately represents the state
306 	 * of the ref.
307 	 * <p>
308 	 * Pending certificates added to this method are not returned by
309 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
310 	 * {@link #save()}.
311 	 *
312 	 * @param cert
313 	 *            certificate to store.
314 	 * @param ident
315 	 *            identity for the commit that stores this certificate. Pending
316 	 *            certificates are sorted by identity timestamp during
317 	 *            {@link #save()}.
318 	 */
319 	public void put(PushCertificate cert, PersonIdent ident) {
320 		put(cert, ident, null);
321 	}
322 
323 	/**
324 	 * Put a certificate to be saved to the store, matching a set of commands.
325 	 * <p>
326 	 * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
327 	 * stored for a push certificate if there is a corresponding command in the
328 	 * list that exactly matches the old/new values mentioned in the push
329 	 * certificate.
330 	 * <p>
331 	 * Pending certificates added to this method are not returned by
332 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
333 	 * {@link #save()}.
334 	 *
335 	 * @param cert
336 	 *            certificate to store.
337 	 * @param ident
338 	 *            identity for the commit that stores this certificate. Pending
339 	 *            certificates are sorted by identity timestamp during
340 	 *            {@link #save()}.
341 	 * @param matching
342 	 *            only store certs for the refs listed in this list whose values
343 	 *            match the commands in the cert.
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 	 * Save pending certificates to the store.
352 	 * <p>
353 	 * One commit is created per certificate added with
354 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
355 	 * timestamps, and a single ref update is performed.
356 	 * <p>
357 	 * The pending list is cleared if and only the ref update fails, which
358 	 * allows for easy retries in case of lock failure.
359 	 *
360 	 * @return the result of attempting to update the ref.
361 	 * @throws java.io.IOException
362 	 *             if there was an error reading from or writing to the
363 	 *             repository.
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 	 * Save pending certificates to the store in an existing batch ref update.
389 	 * <p>
390 	 * One commit is created per certificate added with
391 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
392 	 * timestamps, all commits are flushed, and a single command is added to the
393 	 * batch.
394 	 * <p>
395 	 * The cached ref value and pending list are <em>not</em> cleared. If the
396 	 * ref update succeeds, the caller is responsible for calling
397 	 * {@link #close()} and/or {@link #clear()}.
398 	 *
399 	 * @param batch
400 	 *            update to save to.
401 	 * @return whether a command was added to the batch.
402 	 * @throws java.io.IOException
403 	 *             if there was an error reading from or writing to the
404 	 *             repository.
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 	 * Clear pending certificates added with {@link #put(PushCertificate,
418 	 * PersonIdent)}.
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}"; //$NON-NLS-1$
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 }