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