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 }