View Javadoc
1   /*
2    * Copyright (C) 2008-2012, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.lib;
46  
47  import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
48  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
49  
50  import java.io.IOException;
51  import java.text.MessageFormat;
52  import java.time.Duration;
53  import java.util.ArrayList;
54  import java.util.Arrays;
55  import java.util.Collection;
56  import java.util.Collections;
57  import java.util.HashSet;
58  import java.util.List;
59  import java.util.concurrent.TimeoutException;
60  
61  import org.eclipse.jgit.internal.JGitText;
62  import org.eclipse.jgit.lib.RefUpdate.Result;
63  import org.eclipse.jgit.revwalk.RevWalk;
64  import org.eclipse.jgit.transport.PushCertificate;
65  import org.eclipse.jgit.transport.ReceiveCommand;
66  import org.eclipse.jgit.util.time.ProposedTimestamp;
67  
68  /**
69   * Batch of reference updates to be applied to a repository.
70   * <p>
71   * The batch update is primarily useful in the transport code, where a client or
72   * server is making changes to more than one reference at a time.
73   */
74  public class BatchRefUpdate {
75  	/**
76  	 * Maximum delay the calling thread will tolerate while waiting for a
77  	 * {@code MonotonicClock} to resolve associated {@link ProposedTimestamp}s.
78  	 * <p>
79  	 * A default of 5 seconds was chosen by guessing. A common assumption is
80  	 * clock skew between machines on the same LAN using an NTP server also on
81  	 * the same LAN should be under 5 seconds. 5 seconds is also not that long
82  	 * for a large `git push` operation to complete.
83  	 */
84  	private static final Duration MAX_WAIT = Duration.ofSeconds(5);
85  
86  	private final RefDatabase refdb;
87  
88  	/** Commands to apply during this batch. */
89  	private final List<ReceiveCommand> commands;
90  
91  	/** Does the caller permit a forced update on a reference? */
92  	private boolean allowNonFastForwards;
93  
94  	/** Identity to record action as within the reflog. */
95  	private PersonIdent refLogIdent;
96  
97  	/** Message the caller wants included in the reflog. */
98  	private String refLogMessage;
99  
100 	/** Should the result value be appended to {@link #refLogMessage}. */
101 	private boolean refLogIncludeResult;
102 
103 	/** Push certificate associated with this update. */
104 	private PushCertificate pushCert;
105 
106 	/** Whether updates should be atomic. */
107 	private boolean atomic;
108 
109 	/** Push options associated with this update. */
110 	private List<String> pushOptions;
111 
112 	/** Associated timestamps that should be blocked on before update. */
113 	private List<ProposedTimestamp> timestamps;
114 
115 	/**
116 	 * Initialize a new batch update.
117 	 *
118 	 * @param refdb
119 	 *            the reference database of the repository to be updated.
120 	 */
121 	protected BatchRefUpdate(RefDatabase refdb) {
122 		this.refdb = refdb;
123 		this.commands = new ArrayList<>();
124 		this.atomic = refdb.performsAtomicTransactions();
125 	}
126 
127 	/**
128 	 * @return true if the batch update will permit a non-fast-forward update to
129 	 *         an existing reference.
130 	 */
131 	public boolean isAllowNonFastForwards() {
132 		return allowNonFastForwards;
133 	}
134 
135 	/**
136 	 * Set if this update wants to permit a forced update.
137 	 *
138 	 * @param allow
139 	 *            true if this update batch should ignore merge tests.
140 	 * @return {@code this}.
141 	 */
142 	public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
143 		allowNonFastForwards = allow;
144 		return this;
145 	}
146 
147 	/** @return identity of the user making the change in the reflog. */
148 	public PersonIdent getRefLogIdent() {
149 		return refLogIdent;
150 	}
151 
152 	/**
153 	 * Set the identity of the user appearing in the reflog.
154 	 * <p>
155 	 * The timestamp portion of the identity is ignored. A new identity with the
156 	 * current timestamp will be created automatically when the update occurs
157 	 * and the log record is written.
158 	 *
159 	 * @param pi
160 	 *            identity of the user. If null the identity will be
161 	 *            automatically determined based on the repository
162 	 *            configuration.
163 	 * @return {@code this}.
164 	 */
165 	public BatchRefUpdate setRefLogIdent(final PersonIdent pi) {
166 		refLogIdent = pi;
167 		return this;
168 	}
169 
170 	/**
171 	 * Get the message to include in the reflog.
172 	 *
173 	 * @return message the caller wants to include in the reflog; null if the
174 	 *         update should not be logged.
175 	 */
176 	public String getRefLogMessage() {
177 		return refLogMessage;
178 	}
179 
180 	/** @return {@code true} if the ref log message should show the result. */
181 	public boolean isRefLogIncludingResult() {
182 		return refLogIncludeResult;
183 	}
184 
185 	/**
186 	 * Set the message to include in the reflog.
187 	 *
188 	 * @param msg
189 	 *            the message to describe this change. It may be null if
190 	 *            appendStatus is null in order not to append to the reflog
191 	 * @param appendStatus
192 	 *            true if the status of the ref change (fast-forward or
193 	 *            forced-update) should be appended to the user supplied
194 	 *            message.
195 	 * @return {@code this}.
196 	 */
197 	public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
198 		if (msg == null && !appendStatus)
199 			disableRefLog();
200 		else if (msg == null && appendStatus) {
201 			refLogMessage = ""; //$NON-NLS-1$
202 			refLogIncludeResult = true;
203 		} else {
204 			refLogMessage = msg;
205 			refLogIncludeResult = appendStatus;
206 		}
207 		return this;
208 	}
209 
210 	/**
211 	 * Don't record this update in the ref's associated reflog.
212 	 *
213 	 * @return {@code this}.
214 	 */
215 	public BatchRefUpdate disableRefLog() {
216 		refLogMessage = null;
217 		refLogIncludeResult = false;
218 		return this;
219 	}
220 
221 	/** @return true if log has been disabled by {@link #disableRefLog()}. */
222 	public boolean isRefLogDisabled() {
223 		return refLogMessage == null;
224 	}
225 
226 	/**
227 	 * Request that all updates in this batch be performed atomically.
228 	 * <p>
229 	 * When atomic updates are used, either all commands apply successfully, or
230 	 * none do. Commands that might have otherwise succeeded are rejected with
231 	 * {@code REJECTED_OTHER_REASON}.
232 	 * <p>
233 	 * This method only works if the underlying ref database supports atomic
234 	 * transactions, i.e. {@link RefDatabase#performsAtomicTransactions()} returns
235 	 * true. Calling this method with true if the underlying ref database does not
236 	 * support atomic transactions will cause all commands to fail with {@code
237 	 * REJECTED_OTHER_REASON}.
238 	 *
239 	 * @param atomic whether updates should be atomic.
240 	 * @return {@code this}
241 	 * @since 4.4
242 	 */
243 	public BatchRefUpdate setAtomic(boolean atomic) {
244 		this.atomic = atomic;
245 		return this;
246 	}
247 
248 	/**
249 	 * @return atomic whether updates should be atomic.
250 	 * @since 4.4
251 	 */
252 	public boolean isAtomic() {
253 		return atomic;
254 	}
255 
256 	/**
257 	 * Set a push certificate associated with this update.
258 	 * <p>
259 	 * This usually includes commands to update the refs in this batch, but is not
260 	 * required to.
261 	 *
262 	 * @param cert
263 	 *            push certificate, may be null.
264 	 * @since 4.1
265 	 */
266 	public void setPushCertificate(PushCertificate cert) {
267 		pushCert = cert;
268 	}
269 
270 	/**
271 	 * Set the push certificate associated with this update.
272 	 * <p>
273 	 * This usually includes commands to update the refs in this batch, but is not
274 	 * required to.
275 	 *
276 	 * @return push certificate, may be null.
277 	 * @since 4.1
278 	 */
279 	protected PushCertificate getPushCertificate() {
280 		return pushCert;
281 	}
282 
283 	/** @return commands this update will process. */
284 	public List<ReceiveCommand> getCommands() {
285 		return Collections.unmodifiableList(commands);
286 	}
287 
288 	/**
289 	 * Add a single command to this batch update.
290 	 *
291 	 * @param cmd
292 	 *            the command to add, must not be null.
293 	 * @return {@code this}.
294 	 */
295 	public BatchRefUpdate addCommand(ReceiveCommand cmd) {
296 		commands.add(cmd);
297 		return this;
298 	}
299 
300 	/**
301 	 * Add commands to this batch update.
302 	 *
303 	 * @param cmd
304 	 *            the commands to add, must not be null.
305 	 * @return {@code this}.
306 	 */
307 	public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
308 		return addCommand(Arrays.asList(cmd));
309 	}
310 
311 	/**
312 	 * Add commands to this batch update.
313 	 *
314 	 * @param cmd
315 	 *            the commands to add, must not be null.
316 	 * @return {@code this}.
317 	 */
318 	public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
319 		commands.addAll(cmd);
320 		return this;
321 	}
322 
323 	/**
324 	 * Gets the list of option strings associated with this update.
325 	 *
326 	 * @return pushOptions
327 	 * @since 4.5
328 	 */
329 	public List<String> getPushOptions() {
330 		return pushOptions;
331 	}
332 
333 	/**
334 	 * @return list of timestamps the batch must wait for.
335 	 * @since 4.6
336 	 */
337 	public List<ProposedTimestamp> getProposedTimestamps() {
338 		if (timestamps != null) {
339 			return Collections.unmodifiableList(timestamps);
340 		}
341 		return Collections.emptyList();
342 	}
343 
344 	/**
345 	 * Request the batch to wait for the affected timestamps to resolve.
346 	 *
347 	 * @param ts
348 	 * @return {@code this}.
349 	 * @since 4.6
350 	 */
351 	public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
352 		if (timestamps == null) {
353 			timestamps = new ArrayList<>(4);
354 		}
355 		timestamps.add(ts);
356 		return this;
357 	}
358 
359 	/**
360 	 * Execute this batch update.
361 	 * <p>
362 	 * The default implementation of this method performs a sequential reference
363 	 * update over each reference.
364 	 * <p>
365 	 * Implementations must respect the atomicity requirements of the underlying
366 	 * database as described in {@link #setAtomic(boolean)} and
367 	 * {@link RefDatabase#performsAtomicTransactions()}.
368 	 *
369 	 * @param walk
370 	 *            a RevWalk to parse tags in case the storage system wants to
371 	 *            store them pre-peeled, a common performance optimization.
372 	 * @param monitor
373 	 *            progress monitor to receive update status on.
374 	 * @param options
375 	 *            a list of option strings; set null to execute without
376 	 * @throws IOException
377 	 *             the database is unable to accept the update. Individual
378 	 *             command status must be tested to determine if there is a
379 	 *             partial failure, or a total failure.
380 	 * @since 4.5
381 	 */
382 	public void execute(RevWalk walk, ProgressMonitor monitor,
383 			List<String> options) throws IOException {
384 
385 		if (atomic && !refdb.performsAtomicTransactions()) {
386 			for (ReceiveCommand c : commands) {
387 				if (c.getResult() == NOT_ATTEMPTED) {
388 					c.setResult(REJECTED_OTHER_REASON,
389 							JGitText.get().atomicRefUpdatesNotSupported);
390 				}
391 			}
392 			return;
393 		}
394 		if (!blockUntilTimestamps(MAX_WAIT)) {
395 			return;
396 		}
397 
398 		if (options != null) {
399 			pushOptions = options;
400 		}
401 
402 		monitor.beginTask(JGitText.get().updatingReferences, commands.size());
403 		List<ReceiveCommand> commands2 = new ArrayList<>(
404 				commands.size());
405 		// First delete refs. This may free the name space for some of the
406 		// updates.
407 		for (ReceiveCommand cmd : commands) {
408 			try {
409 				if (cmd.getResult() == NOT_ATTEMPTED) {
410 					cmd.updateType(walk);
411 					switch (cmd.getType()) {
412 					case CREATE:
413 						commands2.add(cmd);
414 						break;
415 					case UPDATE:
416 					case UPDATE_NONFASTFORWARD:
417 						commands2.add(cmd);
418 						break;
419 					case DELETE:
420 						RefUpdate rud = newUpdate(cmd);
421 						monitor.update(1);
422 						cmd.setResult(rud.delete(walk));
423 					}
424 				}
425 			} catch (IOException err) {
426 				cmd.setResult(
427 						REJECTED_OTHER_REASON,
428 						MessageFormat.format(JGitText.get().lockError,
429 								err.getMessage()));
430 			}
431 		}
432 		if (!commands2.isEmpty()) {
433 			// What part of the name space is already taken
434 			Collection<String> takenNames = new HashSet<>(refdb.getRefs(
435 					RefDatabase.ALL).keySet());
436 			Collection<String> takenPrefixes = getTakenPrefixes(takenNames);
437 
438 			// Now to the update that may require more room in the name space
439 			for (ReceiveCommand cmd : commands2) {
440 				try {
441 					if (cmd.getResult() == NOT_ATTEMPTED) {
442 						cmd.updateType(walk);
443 						RefUpdate ru = newUpdate(cmd);
444 						SWITCH: switch (cmd.getType()) {
445 						case DELETE:
446 							// Performed in the first phase
447 							break;
448 						case UPDATE:
449 						case UPDATE_NONFASTFORWARD:
450 							RefUpdate ruu = newUpdate(cmd);
451 							cmd.setResult(ruu.update(walk));
452 							break;
453 						case CREATE:
454 							for (String prefix : getPrefixes(cmd.getRefName())) {
455 								if (takenNames.contains(prefix)) {
456 									cmd.setResult(Result.LOCK_FAILURE);
457 									break SWITCH;
458 								}
459 							}
460 							if (takenPrefixes.contains(cmd.getRefName())) {
461 								cmd.setResult(Result.LOCK_FAILURE);
462 								break SWITCH;
463 							}
464 							ru.setCheckConflicting(false);
465 							addRefToPrefixes(takenPrefixes, cmd.getRefName());
466 							takenNames.add(cmd.getRefName());
467 							cmd.setResult(ru.update(walk));
468 						}
469 					}
470 				} catch (IOException err) {
471 					cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format(
472 							JGitText.get().lockError, err.getMessage()));
473 				} finally {
474 					monitor.update(1);
475 				}
476 			}
477 		}
478 		monitor.endTask();
479 	}
480 
481 	/**
482 	 * Wait for timestamps to be in the past, aborting commands on timeout.
483 	 *
484 	 * @param maxWait
485 	 *            maximum amount of time to wait for timestamps to resolve.
486 	 * @return true if timestamps were successfully waited for; false if
487 	 *         commands were aborted.
488 	 * @since 4.6
489 	 */
490 	protected boolean blockUntilTimestamps(Duration maxWait) {
491 		if (timestamps == null) {
492 			return true;
493 		}
494 		try {
495 			ProposedTimestamp.blockUntil(timestamps, maxWait);
496 			return true;
497 		} catch (TimeoutException | InterruptedException e) {
498 			String msg = JGitText.get().timeIsUncertain;
499 			for (ReceiveCommand c : commands) {
500 				if (c.getResult() == NOT_ATTEMPTED) {
501 					c.setResult(REJECTED_OTHER_REASON, msg);
502 				}
503 			}
504 			return false;
505 		}
506 	}
507 
508 	/**
509 	 * Execute this batch update without option strings.
510 	 *
511 	 * @param walk
512 	 *            a RevWalk to parse tags in case the storage system wants to
513 	 *            store them pre-peeled, a common performance optimization.
514 	 * @param monitor
515 	 *            progress monitor to receive update status on.
516 	 * @throws IOException
517 	 *             the database is unable to accept the update. Individual
518 	 *             command status must be tested to determine if there is a
519 	 *             partial failure, or a total failure.
520 	 */
521 	public void execute(RevWalk walk, ProgressMonitor monitor)
522 			throws IOException {
523 		execute(walk, monitor, null);
524 	}
525 
526 	private static Collection<String> getTakenPrefixes(
527 			final Collection<String> names) {
528 		Collection<String> ref = new HashSet<>();
529 		for (String name : names)
530 			ref.addAll(getPrefixes(name));
531 		return ref;
532 	}
533 
534 	private static void addRefToPrefixes(Collection<String> prefixes,
535 			String name) {
536 		for (String prefix : getPrefixes(name)) {
537 			prefixes.add(prefix);
538 		}
539 	}
540 
541 	static Collection<String> getPrefixes(String s) {
542 		Collection<String> ret = new HashSet<>();
543 		int p1 = s.indexOf('/');
544 		while (p1 > 0) {
545 			ret.add(s.substring(0, p1));
546 			p1 = s.indexOf('/', p1 + 1);
547 		}
548 		return ret;
549 	}
550 
551 	/**
552 	 * Create a new RefUpdate copying the batch settings.
553 	 *
554 	 * @param cmd
555 	 *            specific command the update should be created to copy.
556 	 * @return a single reference update command.
557 	 * @throws IOException
558 	 *             the reference database cannot make a new update object for
559 	 *             the given reference.
560 	 */
561 	protected RefUpdate newUpdate(ReceiveCommand cmd) throws IOException {
562 		RefUpdate ru = refdb.newUpdate(cmd.getRefName(), false);
563 		if (isRefLogDisabled())
564 			ru.disableRefLog();
565 		else {
566 			ru.setRefLogIdent(refLogIdent);
567 			ru.setRefLogMessage(refLogMessage, refLogIncludeResult);
568 		}
569 		ru.setPushCertificate(pushCert);
570 		switch (cmd.getType()) {
571 		case DELETE:
572 			if (!ObjectId.zeroId().equals(cmd.getOldId()))
573 				ru.setExpectedOldObjectId(cmd.getOldId());
574 			ru.setForceUpdate(true);
575 			return ru;
576 
577 		case CREATE:
578 		case UPDATE:
579 		case UPDATE_NONFASTFORWARD:
580 		default:
581 			ru.setForceUpdate(isAllowNonFastForwards());
582 			ru.setExpectedOldObjectId(cmd.getOldId());
583 			ru.setNewObjectId(cmd.getNewId());
584 			return ru;
585 		}
586 	}
587 
588 	@Override
589 	public String toString() {
590 		StringBuilder r = new StringBuilder();
591 		r.append(getClass().getSimpleName()).append('[');
592 		if (commands.isEmpty())
593 			return r.append(']').toString();
594 
595 		r.append('\n');
596 		for (ReceiveCommand cmd : commands) {
597 			r.append("  "); //$NON-NLS-1$
598 			r.append(cmd);
599 			r.append("  (").append(cmd.getResult()); //$NON-NLS-1$
600 			if (cmd.getMessage() != null) {
601 				r.append(": ").append(cmd.getMessage()); //$NON-NLS-1$
602 			}
603 			r.append(")\n"); //$NON-NLS-1$
604 		}
605 		return r.append(']').toString();
606 	}
607 }