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 org.eclipse.jgit.lib.Constants.CHARSET;
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(new InputStreamReader(in, CHARSET))) {
296 			return PushCertificateParser.fromReader(r);
297 		}
298 	}
299 
300 	/**
301 	 * Put a certificate to be saved to the store.
302 	 * <p>
303 	 * Writes the contents of this certificate for each ref mentioned. It is up
304 	 * to the caller to ensure this certificate accurately represents the state
305 	 * of the ref.
306 	 * <p>
307 	 * Pending certificates added to this method are not returned by
308 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
309 	 * {@link #save()}.
310 	 *
311 	 * @param cert
312 	 *            certificate to store.
313 	 * @param ident
314 	 *            identity for the commit that stores this certificate. Pending
315 	 *            certificates are sorted by identity timestamp during
316 	 *            {@link #save()}.
317 	 */
318 	public void put(PushCertificate cert, PersonIdent ident) {
319 		put(cert, ident, null);
320 	}
321 
322 	/**
323 	 * Put a certificate to be saved to the store, matching a set of commands.
324 	 * <p>
325 	 * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
326 	 * stored for a push certificate if there is a corresponding command in the
327 	 * list that exactly matches the old/new values mentioned in the push
328 	 * certificate.
329 	 * <p>
330 	 * Pending certificates added to this method are not returned by
331 	 * {@link #get(String)} and {@link #getAll(String)} until after calling
332 	 * {@link #save()}.
333 	 *
334 	 * @param cert
335 	 *            certificate to store.
336 	 * @param ident
337 	 *            identity for the commit that stores this certificate. Pending
338 	 *            certificates are sorted by identity timestamp during
339 	 *            {@link #save()}.
340 	 * @param matching
341 	 *            only store certs for the refs listed in this list whose values
342 	 *            match the commands in the cert.
343 	 */
344 	public void put(PushCertificate cert, PersonIdent ident,
345 			Collection<ReceiveCommand> matching) {
346 		pending.add(new PendingCert(cert, ident, matching));
347 	}
348 
349 	/**
350 	 * Save pending certificates to the store.
351 	 * <p>
352 	 * One commit is created per certificate added with
353 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
354 	 * timestamps, and a single ref update is performed.
355 	 * <p>
356 	 * The pending list is cleared if and only the ref update fails, which
357 	 * allows for easy retries in case of lock failure.
358 	 *
359 	 * @return the result of attempting to update the ref.
360 	 * @throws java.io.IOException
361 	 *             if there was an error reading from or writing to the
362 	 *             repository.
363 	 */
364 	public RefUpdate.Result save() throws IOException {
365 		ObjectId newId = write();
366 		if (newId == null) {
367 			return RefUpdate.Result.NO_CHANGE;
368 		}
369 		try (ObjectInserter inserter = db.newObjectInserter()) {
370 			RefUpdate.Result result = updateRef(newId);
371 			switch (result) {
372 				case FAST_FORWARD:
373 				case NEW:
374 				case NO_CHANGE:
375 					pending.clear();
376 					break;
377 				default:
378 					break;
379 			}
380 			return result;
381 		} finally {
382 			close();
383 		}
384 	}
385 
386 	/**
387 	 * Save pending certificates to the store in an existing batch ref update.
388 	 * <p>
389 	 * One commit is created per certificate added with
390 	 * {@link #put(PushCertificate, PersonIdent)}, in order of identity
391 	 * timestamps, all commits are flushed, and a single command is added to the
392 	 * batch.
393 	 * <p>
394 	 * The cached ref value and pending list are <em>not</em> cleared. If the
395 	 * ref update succeeds, the caller is responsible for calling
396 	 * {@link #close()} and/or {@link #clear()}.
397 	 *
398 	 * @param batch
399 	 *            update to save to.
400 	 * @return whether a command was added to the batch.
401 	 * @throws java.io.IOException
402 	 *             if there was an error reading from or writing to the
403 	 *             repository.
404 	 */
405 	public boolean save(BatchRefUpdate batch) throws IOException {
406 		ObjectId newId = write();
407 		if (newId == null || newId.equals(commit)) {
408 			return false;
409 		}
410 		batch.addCommand(new ReceiveCommand(
411 				commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
412 		return true;
413 	}
414 
415 	/**
416 	 * Clear pending certificates added with {@link #put(PushCertificate,
417 	 * PersonIdent)}.
418 	 */
419 	public void clear() {
420 		pending.clear();
421 	}
422 
423 	private ObjectId write() throws IOException {
424 		if (pending.isEmpty()) {
425 			return null;
426 		}
427 		if (reader == null) {
428 			load();
429 		}
430 		sortPending(pending);
431 
432 		ObjectId curr = commit;
433 		DirCache dc = newDirCache();
434 		try (ObjectInserter inserter = db.newObjectInserter()) {
435 			for (PendingCert pc : pending) {
436 				curr = saveCert(inserter, dc, pc, curr);
437 			}
438 			inserter.flush();
439 			return curr;
440 		}
441 	}
442 
443 	private static void sortPending(List<PendingCert> pending) {
444 		Collections.sort(pending, new Comparator<PendingCert>() {
445 			@Override
446 			public int compare(PendingCert a, PendingCert b) {
447 				return Long.signum(
448 						a.ident.getWhen().getTime() - b.ident.getWhen().getTime());
449 			}
450 		});
451 	}
452 
453 	private DirCache newDirCache() throws IOException {
454 		if (commit != null) {
455 			return DirCache.read(reader, commit.getTree());
456 		}
457 		return DirCache.newInCore();
458 	}
459 
460 	private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
461 			PendingCert pc, ObjectId curr) throws IOException {
462 		Map<String, ReceiveCommand> byRef;
463 		if (pc.matching != null) {
464 			byRef = new HashMap<>();
465 			for (ReceiveCommand cmd : pc.matching) {
466 				if (byRef.put(cmd.getRefName(), cmd) != null) {
467 					throw new IllegalStateException();
468 				}
469 			}
470 		} else {
471 			byRef = null;
472 		}
473 
474 		DirCacheEditor editor = dc.editor();
475 		String certText = pc.cert.toText() + pc.cert.getSignature();
476 		final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(CHARSET));
477 		boolean any = false;
478 		for (ReceiveCommand cmd : pc.cert.getCommands()) {
479 			if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
480 				continue;
481 			}
482 			any = true;
483 			editor.add(new PathEdit(pathName(cmd.getRefName())) {
484 				@Override
485 				public void apply(DirCacheEntry ent) {
486 					ent.setFileMode(FileMode.REGULAR_FILE);
487 					ent.setObjectId(certId);
488 				}
489 			});
490 		}
491 		if (!any) {
492 			return curr;
493 		}
494 		editor.finish();
495 		CommitBuilder cb = new CommitBuilder();
496 		cb.setAuthor(pc.ident);
497 		cb.setCommitter(pc.ident);
498 		cb.setTreeId(dc.writeTree(inserter));
499 		if (curr != null) {
500 			cb.setParentId(curr);
501 		} else {
502 			cb.setParentIds(Collections.<ObjectId> emptyList());
503 		}
504 		cb.setMessage(buildMessage(pc.cert));
505 		return inserter.insert(OBJ_COMMIT, cb.build());
506 	}
507 
508 	private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
509 		if (c1 == null || c2 == null) {
510 			return c1 == c2;
511 		}
512 		return c1.getRefName().equals(c2.getRefName())
513 				&& c1.getOldId().equals(c2.getOldId())
514 				&& c1.getNewId().equals(c2.getNewId());
515 	}
516 
517 	private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
518 		RefUpdate ru = db.updateRef(REF_NAME);
519 		ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
520 		ru.setNewObjectId(newId);
521 		ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
522 		ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
523 		try (RevWalk rw = new RevWalk(reader)) {
524 			return ru.update(rw);
525 		}
526 	}
527 
528 	private TreeWalk newTreeWalk(String refName) throws IOException {
529 		if (commit == null) {
530 			return null;
531 		}
532 		return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
533 	}
534 
535 	static String pathName(String refName) {
536 		return refName + "@{cert}"; //$NON-NLS-1$
537 	}
538 
539 	private static String buildMessage(PushCertificate cert) {
540 		StringBuilder sb = new StringBuilder();
541 		if (cert.getCommands().size() == 1) {
542 			sb.append(MessageFormat.format(
543 					JGitText.get().storePushCertOneRef,
544 					cert.getCommands().get(0).getRefName()));
545 		} else {
546 			sb.append(MessageFormat.format(
547 					JGitText.get().storePushCertMultipleRefs,
548 					Integer.valueOf(cert.getCommands().size())));
549 		}
550 		return sb.append('\n').toString();
551 	}
552 }