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