View Javadoc
1   /*
2    * Copyright (C) 2008-2012, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
4    *
5    * This program and the accompanying materials are made available under the
6    * terms of the Eclipse Distribution License v. 1.0 which is available at
7    * https://www.eclipse.org/org/documents/edl-v10.php.
8    *
9    * SPDX-License-Identifier: BSD-3-Clause
10   */
11  
12  package org.eclipse.jgit.lib;
13  
14  import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
15  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
16  
17  import java.io.IOException;
18  import java.text.MessageFormat;
19  import java.time.Duration;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.concurrent.TimeoutException;
27  
28  import org.eclipse.jgit.annotations.Nullable;
29  import org.eclipse.jgit.errors.MissingObjectException;
30  import org.eclipse.jgit.internal.JGitText;
31  import org.eclipse.jgit.revwalk.RevWalk;
32  import org.eclipse.jgit.transport.PushCertificate;
33  import org.eclipse.jgit.transport.ReceiveCommand;
34  import org.eclipse.jgit.util.time.ProposedTimestamp;
35  
36  /**
37   * Batch of reference updates to be applied to a repository.
38   * <p>
39   * The batch update is primarily useful in the transport code, where a client or
40   * server is making changes to more than one reference at a time.
41   */
42  public class BatchRefUpdate {
43  	/**
44  	 * Maximum delay the calling thread will tolerate while waiting for a
45  	 * {@code MonotonicClock} to resolve associated {@link ProposedTimestamp}s.
46  	 * <p>
47  	 * A default of 5 seconds was chosen by guessing. A common assumption is
48  	 * clock skew between machines on the same LAN using an NTP server also on
49  	 * the same LAN should be under 5 seconds. 5 seconds is also not that long
50  	 * for a large `git push` operation to complete.
51  	 *
52  	 * @since 4.9
53  	 */
54  	protected static final Duration MAX_WAIT = Duration.ofSeconds(5);
55  
56  	private final RefDatabase refdb;
57  
58  	/** Commands to apply during this batch. */
59  	private final List<ReceiveCommand> commands;
60  
61  	/** Does the caller permit a forced update on a reference? */
62  	private boolean allowNonFastForwards;
63  
64  	/** Identity to record action as within the reflog. */
65  	private PersonIdent refLogIdent;
66  
67  	/** Message the caller wants included in the reflog. */
68  	private String refLogMessage;
69  
70  	/** Should the result value be appended to {@link #refLogMessage}. */
71  	private boolean refLogIncludeResult;
72  
73  	/**
74  	 * Should reflogs be written even if the configured default for this ref is
75  	 * not to write it.
76  	 */
77  	private boolean forceRefLog;
78  
79  	/** Push certificate associated with this update. */
80  	private PushCertificate pushCert;
81  
82  	/** Whether updates should be atomic. */
83  	private boolean atomic;
84  
85  	/** Push options associated with this update. */
86  	private List<String> pushOptions;
87  
88  	/** Associated timestamps that should be blocked on before update. */
89  	private List<ProposedTimestamp> timestamps;
90  
91  	/**
92  	 * Initialize a new batch update.
93  	 *
94  	 * @param refdb
95  	 *            the reference database of the repository to be updated.
96  	 */
97  	protected BatchRefUpdate(RefDatabase refdb) {
98  		this.refdb = refdb;
99  		this.commands = new ArrayList<>();
100 		this.atomic = refdb.performsAtomicTransactions();
101 	}
102 
103 	/**
104 	 * Whether the batch update will permit a non-fast-forward update to an
105 	 * existing reference.
106 	 *
107 	 * @return true if the batch update will permit a non-fast-forward update to
108 	 *         an existing reference.
109 	 */
110 	public boolean isAllowNonFastForwards() {
111 		return allowNonFastForwards;
112 	}
113 
114 	/**
115 	 * Set if this update wants to permit a forced update.
116 	 *
117 	 * @param allow
118 	 *            true if this update batch should ignore merge tests.
119 	 * @return {@code this}.
120 	 */
121 	public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
122 		allowNonFastForwards = allow;
123 		return this;
124 	}
125 
126 	/**
127 	 * Get identity of the user making the change in the reflog.
128 	 *
129 	 * @return identity of the user making the change in the reflog.
130 	 */
131 	public PersonIdent getRefLogIdent() {
132 		return refLogIdent;
133 	}
134 
135 	/**
136 	 * Set the identity of the user appearing in the reflog.
137 	 * <p>
138 	 * The timestamp portion of the identity is ignored. A new identity with the
139 	 * current timestamp will be created automatically when the update occurs
140 	 * and the log record is written.
141 	 *
142 	 * @param pi
143 	 *            identity of the user. If null the identity will be
144 	 *            automatically determined based on the repository
145 	 *            configuration.
146 	 * @return {@code this}.
147 	 */
148 	public BatchRefUpdate setRefLogIdent(PersonIdent pi) {
149 		refLogIdent = pi;
150 		return this;
151 	}
152 
153 	/**
154 	 * Get the message to include in the reflog.
155 	 *
156 	 * @return message the caller wants to include in the reflog; null if the
157 	 *         update should not be logged.
158 	 */
159 	@Nullable
160 	public String getRefLogMessage() {
161 		return refLogMessage;
162 	}
163 
164 	/**
165 	 * Check whether the reflog message should include the result of the update,
166 	 * such as fast-forward or force-update.
167 	 * <p>
168 	 * Describes the default for commands in this batch that do not override it
169 	 * with
170 	 * {@link org.eclipse.jgit.transport.ReceiveCommand#setRefLogMessage(String, boolean)}.
171 	 *
172 	 * @return true if the message should include the result.
173 	 */
174 	public boolean isRefLogIncludingResult() {
175 		return refLogIncludeResult;
176 	}
177 
178 	/**
179 	 * Set the message to include in the reflog.
180 	 * <p>
181 	 * Repository implementations may limit which reflogs are written by
182 	 * default, based on the project configuration. If a repo is not configured
183 	 * to write logs for this ref by default, setting the message alone may have
184 	 * no effect. To indicate that the repo should write logs for this update in
185 	 * spite of configured defaults, use {@link #setForceRefLog(boolean)}.
186 	 * <p>
187 	 * Describes the default for commands in this batch that do not override it
188 	 * with
189 	 * {@link org.eclipse.jgit.transport.ReceiveCommand#setRefLogMessage(String, boolean)}.
190 	 *
191 	 * @param msg
192 	 *            the message to describe this change. If null and appendStatus
193 	 *            is false, the reflog will not be updated.
194 	 * @param appendStatus
195 	 *            true if the status of the ref change (fast-forward or
196 	 *            forced-update) should be appended to the user supplied
197 	 *            message.
198 	 * @return {@code this}.
199 	 */
200 	public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
201 		if (msg == null && !appendStatus)
202 			disableRefLog();
203 		else if (msg == null && appendStatus) {
204 			refLogMessage = ""; //$NON-NLS-1$
205 			refLogIncludeResult = true;
206 		} else {
207 			refLogMessage = msg;
208 			refLogIncludeResult = appendStatus;
209 		}
210 		return this;
211 	}
212 
213 	/**
214 	 * Don't record this update in the ref's associated reflog.
215 	 * <p>
216 	 * Equivalent to {@code setRefLogMessage(null, false)}.
217 	 *
218 	 * @return {@code this}.
219 	 */
220 	public BatchRefUpdate disableRefLog() {
221 		refLogMessage = null;
222 		refLogIncludeResult = false;
223 		return this;
224 	}
225 
226 	/**
227 	 * Force writing a reflog for the updated ref.
228 	 *
229 	 * @param force whether to force.
230 	 * @return {@code this}
231 	 * @since 4.9
232 	 */
233 	public BatchRefUpdate setForceRefLog(boolean force) {
234 		forceRefLog = force;
235 		return this;
236 	}
237 
238 	/**
239 	 * Check whether log has been disabled by {@link #disableRefLog()}.
240 	 *
241 	 * @return true if disabled.
242 	 */
243 	public boolean isRefLogDisabled() {
244 		return refLogMessage == null;
245 	}
246 
247 	/**
248 	 * Check whether the reflog should be written regardless of repo defaults.
249 	 *
250 	 * @return whether force writing is enabled.
251 	 * @since 4.9
252 	 */
253 	protected boolean isForceRefLog() {
254 		return forceRefLog;
255 	}
256 
257 	/**
258 	 * Request that all updates in this batch be performed atomically.
259 	 * <p>
260 	 * When atomic updates are used, either all commands apply successfully, or
261 	 * none do. Commands that might have otherwise succeeded are rejected with
262 	 * {@code REJECTED_OTHER_REASON}.
263 	 * <p>
264 	 * This method only works if the underlying ref database supports atomic
265 	 * transactions, i.e.
266 	 * {@link org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions()}
267 	 * returns true. Calling this method with true if the underlying ref
268 	 * database does not support atomic transactions will cause all commands to
269 	 * fail with {@code
270 	 * REJECTED_OTHER_REASON}.
271 	 *
272 	 * @param atomic
273 	 *            whether updates should be atomic.
274 	 * @return {@code this}
275 	 * @since 4.4
276 	 */
277 	public BatchRefUpdate setAtomic(boolean atomic) {
278 		this.atomic = atomic;
279 		return this;
280 	}
281 
282 	/**
283 	 * Whether updates should be atomic.
284 	 *
285 	 * @return atomic whether updates should be atomic.
286 	 * @since 4.4
287 	 */
288 	public boolean isAtomic() {
289 		return atomic;
290 	}
291 
292 	/**
293 	 * Set a push certificate associated with this update.
294 	 * <p>
295 	 * This usually includes commands to update the refs in this batch, but is not
296 	 * required to.
297 	 *
298 	 * @param cert
299 	 *            push certificate, may be null.
300 	 * @since 4.1
301 	 */
302 	public void setPushCertificate(PushCertificate cert) {
303 		pushCert = cert;
304 	}
305 
306 	/**
307 	 * Set the push certificate associated with this update.
308 	 * <p>
309 	 * This usually includes commands to update the refs in this batch, but is not
310 	 * required to.
311 	 *
312 	 * @return push certificate, may be null.
313 	 * @since 4.1
314 	 */
315 	protected PushCertificate getPushCertificate() {
316 		return pushCert;
317 	}
318 
319 	/**
320 	 * Get commands this update will process.
321 	 *
322 	 * @return commands this update will process.
323 	 */
324 	public List<ReceiveCommand> getCommands() {
325 		return Collections.unmodifiableList(commands);
326 	}
327 
328 	/**
329 	 * Add a single command to this batch update.
330 	 *
331 	 * @param cmd
332 	 *            the command to add, must not be null.
333 	 * @return {@code this}.
334 	 */
335 	public BatchRefUpdate addCommand(ReceiveCommand cmd) {
336 		commands.add(cmd);
337 		return this;
338 	}
339 
340 	/**
341 	 * Add commands to this batch update.
342 	 *
343 	 * @param cmd
344 	 *            the commands to add, must not be null.
345 	 * @return {@code this}.
346 	 */
347 	public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
348 		return addCommand(Arrays.asList(cmd));
349 	}
350 
351 	/**
352 	 * Add commands to this batch update.
353 	 *
354 	 * @param cmd
355 	 *            the commands to add, must not be null.
356 	 * @return {@code this}.
357 	 */
358 	public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
359 		commands.addAll(cmd);
360 		return this;
361 	}
362 
363 	/**
364 	 * Gets the list of option strings associated with this update.
365 	 *
366 	 * @return push options that were passed to {@link #execute}; prior to calling
367 	 *         {@link #execute}, always returns null.
368 	 * @since 4.5
369 	 */
370 	@Nullable
371 	public List<String> getPushOptions() {
372 		return pushOptions;
373 	}
374 
375 	/**
376 	 * Set push options associated with this update.
377 	 * <p>
378 	 * Implementations must call this at the top of {@link #execute(RevWalk,
379 	 * ProgressMonitor, List)}.
380 	 *
381 	 * @param options options passed to {@code execute}.
382 	 * @since 4.9
383 	 */
384 	protected void setPushOptions(List<String> options) {
385 		pushOptions = options;
386 	}
387 
388 	/**
389 	 * Get list of timestamps the batch must wait for.
390 	 *
391 	 * @return list of timestamps the batch must wait for.
392 	 * @since 4.6
393 	 */
394 	public List<ProposedTimestamp> getProposedTimestamps() {
395 		if (timestamps != null) {
396 			return Collections.unmodifiableList(timestamps);
397 		}
398 		return Collections.emptyList();
399 	}
400 
401 	/**
402 	 * Request the batch to wait for the affected timestamps to resolve.
403 	 *
404 	 * @param ts
405 	 *            a {@link org.eclipse.jgit.util.time.ProposedTimestamp} object.
406 	 * @return {@code this}.
407 	 * @since 4.6
408 	 */
409 	public BatchRefUpdate addProposedTimestamp(ProposedTimestamp ts) {
410 		if (timestamps == null) {
411 			timestamps = new ArrayList<>(4);
412 		}
413 		timestamps.add(ts);
414 		return this;
415 	}
416 
417 	/**
418 	 * Execute this batch update.
419 	 * <p>
420 	 * The default implementation of this method performs a sequential reference
421 	 * update over each reference.
422 	 * <p>
423 	 * Implementations must respect the atomicity requirements of the underlying
424 	 * database as described in {@link #setAtomic(boolean)} and
425 	 * {@link org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions()}.
426 	 *
427 	 * @param walk
428 	 *            a RevWalk to parse tags in case the storage system wants to
429 	 *            store them pre-peeled, a common performance optimization.
430 	 * @param monitor
431 	 *            progress monitor to receive update status on.
432 	 * @param options
433 	 *            a list of option strings; set null to execute without
434 	 * @throws java.io.IOException
435 	 *             the database is unable to accept the update. Individual
436 	 *             command status must be tested to determine if there is a
437 	 *             partial failure, or a total failure.
438 	 * @since 4.5
439 	 */
440 	public void execute(RevWalk walk, ProgressMonitor monitor,
441 			List<String> options) throws IOException {
442 
443 		if (atomic && !refdb.performsAtomicTransactions()) {
444 			for (ReceiveCommand c : commands) {
445 				if (c.getResult() == NOT_ATTEMPTED) {
446 					c.setResult(REJECTED_OTHER_REASON,
447 							JGitText.get().atomicRefUpdatesNotSupported);
448 				}
449 			}
450 			return;
451 		}
452 		if (!blockUntilTimestamps(MAX_WAIT)) {
453 			return;
454 		}
455 
456 		if (options != null) {
457 			setPushOptions(options);
458 		}
459 
460 		monitor.beginTask(JGitText.get().updatingReferences, commands.size());
461 		List<ReceiveCommand> commands2 = new ArrayList<>(
462 				commands.size());
463 		// First delete refs. This may free the name space for some of the
464 		// updates.
465 		for (ReceiveCommand cmd : commands) {
466 			try {
467 				if (cmd.getResult() == NOT_ATTEMPTED) {
468 					if (isMissing(walk, cmd.getOldId())
469 							|| isMissing(walk, cmd.getNewId())) {
470 						cmd.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
471 						continue;
472 					}
473 					cmd.updateType(walk);
474 					switch (cmd.getType()) {
475 					case CREATE:
476 						commands2.add(cmd);
477 						break;
478 					case UPDATE:
479 					case UPDATE_NONFASTFORWARD:
480 						commands2.add(cmd);
481 						break;
482 					case DELETE:
483 						RefUpdate rud = newUpdate(cmd);
484 						monitor.update(1);
485 						cmd.setResult(rud.delete(walk));
486 					}
487 				}
488 			} catch (IOException err) {
489 				cmd.setResult(
490 						REJECTED_OTHER_REASON,
491 						MessageFormat.format(JGitText.get().lockError,
492 								err.getMessage()));
493 			}
494 		}
495 		if (!commands2.isEmpty()) {
496 			// Perform updates that may require more room in the name space
497 			for (ReceiveCommand cmd : commands2) {
498 				try {
499 					if (cmd.getResult() == NOT_ATTEMPTED) {
500 						cmd.updateType(walk);
501 						RefUpdate ru = newUpdate(cmd);
502 						switch (cmd.getType()) {
503 							case DELETE:
504 								// Performed in the first phase
505 								break;
506 							case UPDATE:
507 							case UPDATE_NONFASTFORWARD:
508 								RefUpdate ruu = newUpdate(cmd);
509 								cmd.setResult(ruu.update(walk));
510 								break;
511 							case CREATE:
512 								cmd.setResult(ru.update(walk));
513 								break;
514 						}
515 					}
516 				} catch (IOException err) {
517 					cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format(
518 							JGitText.get().lockError, err.getMessage()));
519 				} finally {
520 					monitor.update(1);
521 				}
522 			}
523 		}
524 		monitor.endTask();
525 	}
526 
527 	private static boolean isMissing(RevWalk walk, ObjectId id)
528 			throws IOException {
529 		if (id.equals(ObjectId.zeroId())) {
530 			return false; // Explicit add or delete is not missing.
531 		}
532 		try {
533 			walk.parseAny(id);
534 			return false;
535 		} catch (MissingObjectException e) {
536 			return true;
537 		}
538 	}
539 
540 	/**
541 	 * Wait for timestamps to be in the past, aborting commands on timeout.
542 	 *
543 	 * @param maxWait
544 	 *            maximum amount of time to wait for timestamps to resolve.
545 	 * @return true if timestamps were successfully waited for; false if
546 	 *         commands were aborted.
547 	 * @since 4.6
548 	 */
549 	protected boolean blockUntilTimestamps(Duration maxWait) {
550 		if (timestamps == null) {
551 			return true;
552 		}
553 		try {
554 			ProposedTimestamp.blockUntil(timestamps, maxWait);
555 			return true;
556 		} catch (TimeoutException | InterruptedException e) {
557 			String msg = JGitText.get().timeIsUncertain;
558 			for (ReceiveCommand c : commands) {
559 				if (c.getResult() == NOT_ATTEMPTED) {
560 					c.setResult(REJECTED_OTHER_REASON, msg);
561 				}
562 			}
563 			return false;
564 		}
565 	}
566 
567 	/**
568 	 * Execute this batch update without option strings.
569 	 *
570 	 * @param walk
571 	 *            a RevWalk to parse tags in case the storage system wants to
572 	 *            store them pre-peeled, a common performance optimization.
573 	 * @param monitor
574 	 *            progress monitor to receive update status on.
575 	 * @throws java.io.IOException
576 	 *             the database is unable to accept the update. Individual
577 	 *             command status must be tested to determine if there is a
578 	 *             partial failure, or a total failure.
579 	 */
580 	public void execute(RevWalk walk, ProgressMonitor monitor)
581 			throws IOException {
582 		execute(walk, monitor, null);
583 	}
584 
585 	/**
586 	 * Get all path prefixes of a ref name.
587 	 *
588 	 * @param name
589 	 *            ref name.
590 	 * @return path prefixes of the ref name. For {@code refs/heads/foo}, returns
591 	 *         {@code refs} and {@code refs/heads}.
592 	 * @since 4.9
593 	 */
594 	protected static Collection<String> getPrefixes(String name) {
595 		Collection<String> ret = new HashSet<>();
596 		addPrefixesTo(name, ret);
597 		return ret;
598 	}
599 
600 	/**
601 	 * Add prefixes of a ref name to an existing collection.
602 	 *
603 	 * @param name
604 	 *            ref name.
605 	 * @param out
606 	 *            path prefixes of the ref name. For {@code refs/heads/foo},
607 	 *            returns {@code refs} and {@code refs/heads}.
608 	 * @since 4.9
609 	 */
610 	protected static void addPrefixesTo(String name, Collection<String> out) {
611 		int p1 = name.indexOf('/');
612 		while (p1 > 0) {
613 			out.add(name.substring(0, p1));
614 			p1 = name.indexOf('/', p1 + 1);
615 		}
616 	}
617 
618 	/**
619 	 * Create a new RefUpdate copying the batch settings.
620 	 *
621 	 * @param cmd
622 	 *            specific command the update should be created to copy.
623 	 * @return a single reference update command.
624 	 * @throws java.io.IOException
625 	 *             the reference database cannot make a new update object for
626 	 *             the given reference.
627 	 */
628 	protected RefUpdate newUpdate(ReceiveCommand cmd) throws IOException {
629 		RefUpdate ru = refdb.newUpdate(cmd.getRefName(), false);
630 		if (isRefLogDisabled(cmd)) {
631 			ru.disableRefLog();
632 		} else {
633 			ru.setRefLogIdent(refLogIdent);
634 			ru.setRefLogMessage(getRefLogMessage(cmd), isRefLogIncludingResult(cmd));
635 			ru.setForceRefLog(isForceRefLog(cmd));
636 		}
637 		ru.setPushCertificate(pushCert);
638 		switch (cmd.getType()) {
639 		case DELETE:
640 			if (!ObjectId.zeroId().equals(cmd.getOldId()))
641 				ru.setExpectedOldObjectId(cmd.getOldId());
642 			ru.setForceUpdate(true);
643 			return ru;
644 
645 		case CREATE:
646 		case UPDATE:
647 		case UPDATE_NONFASTFORWARD:
648 		default:
649 			ru.setForceUpdate(isAllowNonFastForwards());
650 			ru.setExpectedOldObjectId(cmd.getOldId());
651 			ru.setNewObjectId(cmd.getNewId());
652 			return ru;
653 		}
654 	}
655 
656 	/**
657 	 * Check whether reflog is disabled for a command.
658 	 *
659 	 * @param cmd
660 	 *            specific command.
661 	 * @return whether the reflog is disabled, taking into account the state from
662 	 *         this instance as well as overrides in the given command.
663 	 * @since 4.9
664 	 */
665 	protected boolean isRefLogDisabled(ReceiveCommand cmd) {
666 		return cmd.hasCustomRefLog() ? cmd.isRefLogDisabled() : isRefLogDisabled();
667 	}
668 
669 	/**
670 	 * Get reflog message for a command.
671 	 *
672 	 * @param cmd
673 	 *            specific command.
674 	 * @return reflog message, taking into account the state from this instance as
675 	 *         well as overrides in the given command.
676 	 * @since 4.9
677 	 */
678 	protected String getRefLogMessage(ReceiveCommand cmd) {
679 		return cmd.hasCustomRefLog() ? cmd.getRefLogMessage() : getRefLogMessage();
680 	}
681 
682 	/**
683 	 * Check whether the reflog message for a command should include the result.
684 	 *
685 	 * @param cmd
686 	 *            specific command.
687 	 * @return whether the reflog message should show the result, taking into
688 	 *         account the state from this instance as well as overrides in the
689 	 *         given command.
690 	 * @since 4.9
691 	 */
692 	protected boolean isRefLogIncludingResult(ReceiveCommand cmd) {
693 		return cmd.hasCustomRefLog()
694 				? cmd.isRefLogIncludingResult() : isRefLogIncludingResult();
695 	}
696 
697 	/**
698 	 * Check whether the reflog for a command should be written regardless of repo
699 	 * defaults.
700 	 *
701 	 * @param cmd
702 	 *            specific command.
703 	 * @return whether force writing is enabled.
704 	 * @since 4.9
705 	 */
706 	protected boolean isForceRefLog(ReceiveCommand cmd) {
707 		Boolean isForceRefLog = cmd.isForceRefLog();
708 		return isForceRefLog != null ? isForceRefLog.booleanValue()
709 				: isForceRefLog();
710 	}
711 
712 	/** {@inheritDoc} */
713 	@Override
714 	public String toString() {
715 		StringBuilder r = new StringBuilder();
716 		r.append(getClass().getSimpleName()).append('[');
717 		if (commands.isEmpty())
718 			return r.append(']').toString();
719 
720 		r.append('\n');
721 		for (ReceiveCommand cmd : commands) {
722 			r.append("  "); //$NON-NLS-1$
723 			r.append(cmd);
724 			r.append("  (").append(cmd.getResult()); //$NON-NLS-1$
725 			if (cmd.getMessage() != null) {
726 				r.append(": ").append(cmd.getMessage()); //$NON-NLS-1$
727 			}
728 			r.append(")\n"); //$NON-NLS-1$
729 		}
730 		return r.append(']').toString();
731 	}
732 }