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