Do not write to the reflog unless the refupdate logmessage is set
[jgit.git] / org.spearce.jgit / src / org / spearce / jgit / lib / RefUpdate.java
blob8044c6eae8fffaf5315fe2bec6affcb04724a72b
1 /*
2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
18 * - Neither the name of the Git Development Community nor the
19 * names of its contributors may be used to endorse or promote
20 * products derived from this software without specific prior
21 * written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
26 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
30 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
35 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 package org.spearce.jgit.lib;
40 import java.io.File;
41 import java.io.IOException;
43 import org.spearce.jgit.errors.MissingObjectException;
44 import org.spearce.jgit.lib.Ref.Storage;
45 import org.spearce.jgit.revwalk.RevCommit;
46 import org.spearce.jgit.revwalk.RevObject;
47 import org.spearce.jgit.revwalk.RevWalk;
49 /**
50 * Updates any locally stored ref.
52 public class RefUpdate {
53 /** Status of an update request. */
54 public static enum Result {
55 /** The ref update/delete has not been attempted by the caller. */
56 NOT_ATTEMPTED,
58 /**
59 * The ref could not be locked for update/delete.
60 * <p>
61 * This is generally a transient failure and is usually caused by
62 * another process trying to access the ref at the same time as this
63 * process was trying to update it. It is possible a future operation
64 * will be successful.
66 LOCK_FAILURE,
68 /**
69 * Same value already stored.
70 * <p>
71 * Both the old value and the new value are identical. No change was
72 * necessary for an update. For delete the branch is removed.
74 NO_CHANGE,
76 /**
77 * The ref was created locally for an update, but ignored for delete.
78 * <p>
79 * The ref did not exist when the update started, but it was created
80 * successfully with the new value.
82 NEW,
84 /**
85 * The ref had to be forcefully updated/deleted.
86 * <p>
87 * The ref already existed but its old value was not fully merged into
88 * the new value. The configuration permitted a forced update to take
89 * place, so ref now contains the new value. History associated with the
90 * objects not merged may no longer be reachable.
92 FORCED,
94 /**
95 * The ref was updated/deleted in a fast-forward way.
96 * <p>
97 * The tracking ref already existed and its old value was fully merged
98 * into the new value. No history was made unreachable.
100 FAST_FORWARD,
103 * Not a fast-forward and not stored.
104 * <p>
105 * The tracking ref already existed but its old value was not fully
106 * merged into the new value. The configuration did not allow a forced
107 * update/delete to take place, so ref still contains the old value. No
108 * previous history was lost.
110 REJECTED,
113 * Rejected because trying to delete the current branch.
114 * <p>
115 * Has no meaning for update.
117 REJECTED_CURRENT_BRANCH,
120 * The ref was probably not updated/deleted because of I/O error.
121 * <p>
122 * Unexpected I/O error occurred when writing new ref. Such error may
123 * result in uncertain state, but most probably ref was not updated.
124 * <p>
125 * This kind of error doesn't include {@link #LOCK_FAILURE}, which is a
126 * different case.
128 IO_FAILURE
131 /** Repository the ref is stored in. */
132 private final RefDatabase db;
134 /** Location of the loose file holding the value of this ref. */
135 private final File looseFile;
137 /** New value the caller wants this ref to have. */
138 private ObjectId newValue;
140 /** Does this specification ask for forced updated (rewind/reset)? */
141 private boolean force;
143 /** Identity to record action as within the reflog. */
144 private PersonIdent refLogIdent;
146 /** Message the caller wants included in the reflog. */
147 private String refLogMessage;
149 /** Should the Result value be appended to {@link #refLogMessage}. */
150 private boolean refLogIncludeResult;
152 /** Old value of the ref, obtained after we lock it. */
153 private ObjectId oldValue;
155 /** If non-null, the value {@link #oldValue} must have to continue. */
156 private ObjectId expValue;
158 /** Result of the update operation. */
159 private Result result = Result.NOT_ATTEMPTED;
161 private final Ref ref;
163 RefUpdate(final RefDatabase r, final Ref ref, final File f) {
164 db = r;
165 this.ref = ref;
166 oldValue = ref.getObjectId();
167 looseFile = f;
168 refLogMessage = "";
171 /** @return the repository the updated ref resides in */
172 public Repository getRepository() {
173 return db.getRepository();
177 * Get the name of the ref this update will operate on.
179 * @return name of this ref.
181 public String getName() {
182 return ref.getName();
186 * Get the new value the ref will be (or was) updated to.
188 * @return new value. Null if the caller has not configured it.
190 public ObjectId getNewObjectId() {
191 return newValue;
195 * Set the new value the ref will update to.
197 * @param id
198 * the new value.
200 public void setNewObjectId(final AnyObjectId id) {
201 newValue = id.copy();
205 * @return the expected value of the ref after the lock is taken, but before
206 * update occurs. Null to avoid the compare and swap test. Use
207 * {@link ObjectId#zeroId()} to indicate expectation of a
208 * non-existant ref.
210 public ObjectId getExpectedOldObjectId() {
211 return expValue;
215 * @param id
216 * the expected value of the ref after the lock is taken, but
217 * before update occurs. Null to avoid the compare and swap test.
218 * Use {@link ObjectId#zeroId()} to indicate expectation of a
219 * non-existant ref.
221 public void setExpectedOldObjectId(final AnyObjectId id) {
222 expValue = id != null ? id.toObjectId() : null;
226 * Check if this update wants to forcefully change the ref.
228 * @return true if this update should ignore merge tests.
230 public boolean isForceUpdate() {
231 return force;
235 * Set if this update wants to forcefully change the ref.
237 * @param b
238 * true if this update should ignore merge tests.
240 public void setForceUpdate(final boolean b) {
241 force = b;
244 /** @return identity of the user making the change in the reflog. */
245 public PersonIdent getRefLogIdent() {
246 return refLogIdent;
250 * Set the identity of the user appearing in the reflog.
251 * <p>
252 * The timestamp portion of the identity is ignored. A new identity with the
253 * current timestamp will be created automatically when the update occurs
254 * and the log record is written.
256 * @param pi
257 * identity of the user. If null the identity will be
258 * automatically determined based on the repository
259 * configuration.
261 public void setRefLogIdent(final PersonIdent pi) {
262 refLogIdent = pi;
266 * Get the message to include in the reflog.
268 * @return message the caller wants to include in the reflog; null if the
269 * update should not be logged.
271 public String getRefLogMessage() {
272 return refLogMessage;
276 * Set the message to include in the reflog.
278 * @param msg
279 * the message to describe this change. It may be null
280 * if appendStatus is null in order not to append to the reflog
281 * @param appendStatus
282 * true if the status of the ref change (fast-forward or
283 * forced-update) should be appended to the user supplied
284 * message.
286 public void setRefLogMessage(final String msg, final boolean appendStatus) {
287 if (msg == null && !appendStatus)
288 disableRefLog();
289 else if (msg == null && appendStatus) {
290 refLogMessage = "";
291 refLogIncludeResult = true;
292 } else {
293 refLogMessage = msg;
294 refLogIncludeResult = appendStatus;
298 /** Don't record this update in the ref's associated reflog. */
299 public void disableRefLog() {
300 refLogMessage = null;
301 refLogIncludeResult = false;
305 * The old value of the ref, prior to the update being attempted.
306 * <p>
307 * This value may differ before and after the update method. Initially it is
308 * populated with the value of the ref before the lock is taken, but the old
309 * value may change if someone else modified the ref between the time we
310 * last read it and when the ref was locked for update.
312 * @return the value of the ref prior to the update being attempted; null if
313 * the updated has not been attempted yet.
315 public ObjectId getOldObjectId() {
316 return oldValue;
320 * Get the status of this update.
321 * <p>
322 * The same value that was previously returned from an update method.
324 * @return the status of the update.
326 public Result getResult() {
327 return result;
330 private void requireCanDoUpdate() {
331 if (newValue == null)
332 throw new IllegalStateException("A NewObjectId is required.");
336 * Force the ref to take the new value.
337 * <p>
338 * This is just a convenient helper for setting the force flag, and as such
339 * the merge test is performed.
341 * @return the result status of the update.
342 * @throws IOException
343 * an unexpected IO error occurred while writing changes.
345 public Result forceUpdate() throws IOException {
346 force = true;
347 return update();
351 * Gracefully update the ref to the new value.
352 * <p>
353 * Merge test will be performed according to {@link #isForceUpdate()}.
354 * <p>
355 * This is the same as:
357 * <pre>
358 * return update(new RevWalk(repository));
359 * </pre>
361 * @return the result status of the update.
362 * @throws IOException
363 * an unexpected IO error occurred while writing changes.
365 public Result update() throws IOException {
366 return update(new RevWalk(db.getRepository()));
370 * Gracefully update the ref to the new value.
371 * <p>
372 * Merge test will be performed according to {@link #isForceUpdate()}.
374 * @param walk
375 * a RevWalk instance this update command can borrow to perform
376 * the merge test. The walk will be reset to perform the test.
377 * @return the result status of the update.
378 * @throws IOException
379 * an unexpected IO error occurred while writing changes.
381 public Result update(final RevWalk walk) throws IOException {
382 requireCanDoUpdate();
383 try {
384 return result = updateImpl(walk, new UpdateStore());
385 } catch (IOException x) {
386 result = Result.IO_FAILURE;
387 throw x;
392 * Delete the ref.
393 * <p>
394 * This is the same as:
396 * <pre>
397 * return delete(new RevWalk(repository));
398 * </pre>
400 * @return the result status of the delete.
401 * @throws IOException
403 public Result delete() throws IOException {
404 return delete(new RevWalk(db.getRepository()));
408 * Delete the ref.
410 * @param walk
411 * a RevWalk instance this delete command can borrow to perform
412 * the merge test. The walk will be reset to perform the test.
413 * @return the result status of the delete.
414 * @throws IOException
416 public Result delete(final RevWalk walk) throws IOException {
417 if (getName().startsWith(Constants.R_HEADS)) {
418 final Ref head = db.readRef(Constants.HEAD);
419 if (head != null && getName().equals(head.getName()))
420 return result = Result.REJECTED_CURRENT_BRANCH;
423 try {
424 return result = updateImpl(walk, new DeleteStore());
425 } catch (IOException x) {
426 result = Result.IO_FAILURE;
427 throw x;
431 private Result updateImpl(final RevWalk walk, final Store store)
432 throws IOException {
433 final LockFile lock;
434 RevObject newObj;
435 RevObject oldObj;
437 lock = new LockFile(looseFile);
438 if (!lock.lock())
439 return Result.LOCK_FAILURE;
440 try {
441 oldValue = db.idOf(getName());
442 if (expValue != null) {
443 final ObjectId o;
444 o = oldValue != null ? oldValue : ObjectId.zeroId();
445 if (!expValue.equals(o))
446 return Result.LOCK_FAILURE;
448 if (oldValue == null)
449 return store.store(lock, Result.NEW);
451 newObj = safeParse(walk, newValue);
452 oldObj = safeParse(walk, oldValue);
453 if (newObj == oldObj)
454 return store.store(lock, Result.NO_CHANGE);
456 if (newObj instanceof RevCommit && oldObj instanceof RevCommit) {
457 if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj))
458 return store.store(lock, Result.FAST_FORWARD);
461 if (isForceUpdate())
462 return store.store(lock, Result.FORCED);
463 return Result.REJECTED;
464 } finally {
465 lock.unlock();
469 private static RevObject safeParse(final RevWalk rw, final AnyObjectId id)
470 throws IOException {
471 try {
472 return id != null ? rw.parseAny(id) : null;
473 } catch (MissingObjectException e) {
474 // We can expect some objects to be missing, like if we are
475 // trying to force a deletion of a branch and the object it
476 // points to has been pruned from the database due to freak
477 // corruption accidents (it happens with 'git new-work-dir').
479 return null;
483 private Result updateStore(final LockFile lock, final Result status)
484 throws IOException {
485 if (status == Result.NO_CHANGE)
486 return status;
487 lock.setNeedStatInformation(true);
488 lock.write(newValue);
489 String msg = getRefLogMessage();
490 if (msg != null) {
491 if (refLogIncludeResult) {
492 String strResult = toResultString(status);
493 if (strResult != null) {
494 if (msg.length() > 0)
495 msg = msg + ": " + strResult;
496 else
497 msg = strResult;
500 RefLogWriter.append(this, msg);
502 if (!lock.commit())
503 return Result.LOCK_FAILURE;
504 db.stored(this.ref.getOrigName(), ref.getName(), newValue, lock.getCommitLastModified());
505 return status;
508 private static String toResultString(final Result status) {
509 switch (status) {
510 case FORCED:
511 return "forced-update";
512 case FAST_FORWARD:
513 return "fast forward";
514 case NEW:
515 return "created";
516 default:
517 return null;
522 * Handle the abstraction of storing a ref update. This is because both
523 * updating and deleting of a ref have merge testing in common.
525 private abstract class Store {
526 abstract Result store(final LockFile lock, final Result status)
527 throws IOException;
530 private class UpdateStore extends Store {
532 @Override
533 Result store(final LockFile lock, final Result status)
534 throws IOException {
535 return updateStore(lock, status);
539 private class DeleteStore extends Store {
541 @Override
542 Result store(LockFile lock, Result status) throws IOException {
543 Storage storage = ref.getStorage();
544 if (storage == Storage.NEW)
545 return status;
546 if (storage.isPacked())
547 db.removePackedRef(ref.getName());
549 final int levels = count(ref.getName(), '/') - 2;
551 // Delete logs _before_ unlocking
552 final File gitDir = db.getRepository().getDirectory();
553 final File logDir = new File(gitDir, Constants.LOGS);
554 deleteFileAndEmptyDir(new File(logDir, ref.getName()), levels);
556 // We have to unlock before (maybe) deleting the parent directories
557 lock.unlock();
558 if (storage.isLoose())
559 deleteFileAndEmptyDir(looseFile, levels);
560 return status;
563 private void deleteFileAndEmptyDir(final File file, final int depth)
564 throws IOException {
565 if (file.exists()) {
566 if (!file.delete())
567 throw new IOException("File cannot be deleted: " + file);
568 deleteEmptyDir(file.getParentFile(), depth);
572 private void deleteEmptyDir(File dir, int depth) {
573 for (; depth > 0 && dir != null; depth--) {
574 if (!dir.delete())
575 break;
576 dir = dir.getParentFile();
581 private static int count(final String s, final char c) {
582 int count = 0;
583 for (int p = s.indexOf(c); p >= 0; p = s.indexOf(c, p + 1)) {
584 count++;
586 return count;