2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
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
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
;
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
;
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. */
59 * The ref could not be locked for update/delete.
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
69 * Same value already stored.
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.
77 * The ref was created locally for an update, but ignored for delete.
79 * The ref did not exist when the update started, but it was created
80 * successfully with the new value.
85 * The ref had to be forcefully updated/deleted.
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.
95 * The ref was updated/deleted in a fast-forward way.
97 * The tracking ref already existed and its old value was fully merged
98 * into the new value. No history was made unreachable.
103 * Not a fast-forward and not stored.
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.
113 * Rejected because trying to delete the current branch.
115 * Has no meaning for update.
117 REJECTED_CURRENT_BRANCH
,
120 * The ref was probably not updated/deleted because of I/O error.
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.
125 * This kind of error doesn't include {@link #LOCK_FAILURE}, which is a
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
) {
166 oldValue
= ref
.getObjectId();
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() {
195 * Set the new value the ref will update to.
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
210 public ObjectId
getExpectedOldObjectId() {
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
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() {
235 * Set if this update wants to forcefully change the ref.
238 * true if this update should ignore merge tests.
240 public void setForceUpdate(final boolean b
) {
244 /** @return identity of the user making the change in the reflog. */
245 public PersonIdent
getRefLogIdent() {
250 * Set the identity of the user appearing in the reflog.
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.
257 * identity of the user. If null the identity will be
258 * automatically determined based on the repository
261 public void setRefLogIdent(final PersonIdent 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.
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
286 public void setRefLogMessage(final String msg
, final boolean appendStatus
) {
287 if (msg
== null && !appendStatus
)
289 else if (msg
== null && appendStatus
) {
291 refLogIncludeResult
= true;
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.
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() {
320 * Get the status of this update.
322 * The same value that was previously returned from an update method.
324 * @return the status of the update.
326 public Result
getResult() {
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.
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
{
351 * Gracefully update the ref to the new value.
353 * Merge test will be performed according to {@link #isForceUpdate()}.
355 * This is the same as:
358 * return update(new RevWalk(repository));
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.
372 * Merge test will be performed according to {@link #isForceUpdate()}.
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();
384 return result
= updateImpl(walk
, new UpdateStore());
385 } catch (IOException x
) {
386 result
= Result
.IO_FAILURE
;
394 * This is the same as:
397 * return delete(new RevWalk(repository));
400 * @return the result status of the delete.
401 * @throws IOException
403 public Result
delete() throws IOException
{
404 return delete(new RevWalk(db
.getRepository()));
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
;
424 return result
= updateImpl(walk
, new DeleteStore());
425 } catch (IOException x
) {
426 result
= Result
.IO_FAILURE
;
431 private Result
updateImpl(final RevWalk walk
, final Store store
)
437 lock
= new LockFile(looseFile
);
439 return Result
.LOCK_FAILURE
;
441 oldValue
= db
.idOf(getName());
442 if (expValue
!= null) {
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
);
462 return store
.store(lock
, Result
.FORCED
);
463 return Result
.REJECTED
;
469 private static RevObject
safeParse(final RevWalk rw
, final AnyObjectId id
)
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').
483 private Result
updateStore(final LockFile lock
, final Result status
)
485 if (status
== Result
.NO_CHANGE
)
487 lock
.setNeedStatInformation(true);
488 lock
.write(newValue
);
489 String msg
= getRefLogMessage();
491 if (refLogIncludeResult
) {
492 String strResult
= toResultString(status
);
493 if (strResult
!= null) {
494 if (msg
.length() > 0)
495 msg
= msg
+ ": " + strResult
;
500 RefLogWriter
.append(this, msg
);
503 return Result
.LOCK_FAILURE
;
504 db
.stored(this.ref
.getOrigName(), ref
.getName(), newValue
, lock
.getCommitLastModified());
508 private static String
toResultString(final Result status
) {
511 return "forced-update";
513 return "fast forward";
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
)
530 private class UpdateStore
extends Store
{
533 Result
store(final LockFile lock
, final Result status
)
535 return updateStore(lock
, status
);
539 private class DeleteStore
extends Store
{
542 Result
store(LockFile lock
, Result status
) throws IOException
{
543 Storage storage
= ref
.getStorage();
544 if (storage
== Storage
.NEW
)
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
558 if (storage
.isLoose())
559 deleteFileAndEmptyDir(looseFile
, levels
);
563 private void deleteFileAndEmptyDir(final File file
, final int depth
)
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
--) {
576 dir
= dir
.getParentFile();
581 private static int count(final String s
, final char c
) {
583 for (int p
= s
.indexOf(c
); p
>= 0; p
= s
.indexOf(c
, p
+ 1)) {