RefUpdate.java
/*
* Copyright (C) 2008-2010, Google Inc.
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.lib;
import java.io.IOException;
import java.text.MessageFormat;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.util.References;
/**
* Creates, updates or deletes any reference.
*/
public abstract class RefUpdate {
/**
* Status of an update request.
* <p>
* New values may be added to this enum in the future. Callers may assume that
* unknown values are failures, and may generally treat them the same as
* {@link #REJECTED_OTHER_REASON}.
*/
public enum Result {
/** The ref update/delete has not been attempted by the caller. */
NOT_ATTEMPTED,
/**
* The ref could not be locked for update/delete.
* <p>
* This is generally a transient failure and is usually caused by
* another process trying to access the ref at the same time as this
* process was trying to update it. It is possible a future operation
* will be successful.
*/
LOCK_FAILURE,
/**
* Same value already stored.
* <p>
* Both the old value and the new value are identical. No change was
* necessary for an update. For delete the branch is removed.
*/
NO_CHANGE,
/**
* The ref was created locally for an update, but ignored for delete.
* <p>
* The ref did not exist when the update started, but it was created
* successfully with the new value.
*/
NEW,
/**
* The ref had to be forcefully updated/deleted.
* <p>
* The ref already existed but its old value was not fully merged into
* the new value. The configuration permitted a forced update to take
* place, so ref now contains the new value. History associated with the
* objects not merged may no longer be reachable.
*/
FORCED,
/**
* The ref was updated/deleted in a fast-forward way.
* <p>
* The tracking ref already existed and its old value was fully merged
* into the new value. No history was made unreachable.
*/
FAST_FORWARD,
/**
* Not a fast-forward and not stored.
* <p>
* The tracking ref already existed but its old value was not fully
* merged into the new value. The configuration did not allow a forced
* update/delete to take place, so ref still contains the old value. No
* previous history was lost.
* <p>
* <em>Note:</em> Despite the general name, this result only refers to the
* non-fast-forward case. For more general errors, see {@link
* #REJECTED_OTHER_REASON}.
*/
REJECTED,
/**
* Rejected because trying to delete the current branch.
* <p>
* Has no meaning for update.
*/
REJECTED_CURRENT_BRANCH,
/**
* The ref was probably not updated/deleted because of I/O error.
* <p>
* Unexpected I/O error occurred when writing new ref. Such error may
* result in uncertain state, but most probably ref was not updated.
* <p>
* This kind of error doesn't include {@link #LOCK_FAILURE}, which is a
* different case.
*/
IO_FAILURE,
/**
* The ref was renamed from another name
* <p>
*/
RENAMED,
/**
* One or more objects aren't in the repository.
* <p>
* This is severe indication of either repository corruption on the
* server side, or a bug in the client wherein the client did not supply
* all required objects during the pack transfer.
*
* @since 4.9
*/
REJECTED_MISSING_OBJECT,
/**
* Rejected for some other reason not covered by another enum value.
*
* @since 4.9
*/
REJECTED_OTHER_REASON;
}
/** New value the caller wants this ref to have. */
private ObjectId newValue;
/** Does this specification ask for forced updated (rewind/reset)? */
private boolean force;
/** Identity to record action as within the reflog. */
private PersonIdent refLogIdent;
/** Message the caller wants included in the reflog. */
private String refLogMessage;
/** Should the Result value be appended to {@link #refLogMessage}. */
private boolean refLogIncludeResult;
/**
* Should reflogs be written even if the configured default for this ref is
* not to write it.
*/
private boolean forceRefLog;
/** Old value of the ref, obtained after we lock it. */
private ObjectId oldValue;
/** If non-null, the value {@link #oldValue} must have to continue. */
private ObjectId expValue;
/** Result of the update operation. */
private Result result = Result.NOT_ATTEMPTED;
/** Push certificate associated with this update. */
private PushCertificate pushCert;
private final Ref ref;
/**
* Is this RefUpdate detaching a symbolic ref?
*
* We need this info since this.ref will normally be peeled of in case of
* detaching a symbolic ref (HEAD for example).
*
* Without this flag we cannot decide whether the ref has to be updated or
* not in case when it was a symbolic ref and the newValue == oldValue.
*/
private boolean detachingSymbolicRef;
private boolean checkConflicting = true;
/**
* Construct a new update operation for the reference.
* <p>
* {@code ref.getObjectId()} will be used to seed {@link #getOldObjectId()},
* which callers can use as part of their own update logic.
*
* @param ref
* the reference that will be updated by this operation.
*/
protected RefUpdate(Ref ref) {
this.ref = ref;
oldValue = ref.getObjectId();
refLogMessage = ""; //$NON-NLS-1$
}
/**
* Get the reference database this update modifies.
*
* @return the reference database this update modifies.
*/
protected abstract RefDatabase getRefDatabase();
/**
* Get the repository storing the database's objects.
*
* @return the repository storing the database's objects.
*/
protected abstract Repository getRepository();
/**
* Try to acquire the lock on the reference.
* <p>
* If the locking was successful the implementor must set the current
* identity value by calling {@link #setOldObjectId(ObjectId)}.
*
* @param deref
* true if the lock should be taken against the leaf level
* reference; false if it should be taken exactly against the
* current reference.
* @return true if the lock was acquired and the reference is likely
* protected from concurrent modification; false if it failed.
* @throws java.io.IOException
* the lock couldn't be taken due to an unexpected storage
* failure, and not because of a concurrent update.
*/
protected abstract boolean tryLock(boolean deref) throws IOException;
/**
* Releases the lock taken by {@link #tryLock} if it succeeded.
*/
protected abstract void unlock();
/**
* Do update
*
* @param desiredResult
* a {@link org.eclipse.jgit.lib.RefUpdate.Result} object.
* @return {@code result}
* @throws java.io.IOException
*/
protected abstract Result doUpdate(Result desiredResult) throws IOException;
/**
* Do delete
*
* @param desiredResult
* a {@link org.eclipse.jgit.lib.RefUpdate.Result} object.
* @return {@code result}
* @throws java.io.IOException
*/
protected abstract Result doDelete(Result desiredResult) throws IOException;
/**
* Do link
*
* @param target
* a {@link java.lang.String} object.
* @return {@link org.eclipse.jgit.lib.RefUpdate.Result#NEW} on success.
* @throws java.io.IOException
*/
protected abstract Result doLink(String target) throws IOException;
/**
* Get the name of the ref this update will operate on.
*
* @return name of underlying ref.
*/
public String getName() {
return getRef().getName();
}
/**
* Get the reference this update will create or modify.
*
* @return the reference this update will create or modify.
*/
public Ref getRef() {
return ref;
}
/**
* Get the new value the ref will be (or was) updated to.
*
* @return new value. Null if the caller has not configured it.
*/
public ObjectId getNewObjectId() {
return newValue;
}
/**
* Tells this RefUpdate that it is actually detaching a symbolic ref.
*/
public void setDetachingSymbolicRef() {
detachingSymbolicRef = true;
}
/**
* Return whether this update is actually detaching a symbolic ref.
*
* @return true if detaching a symref.
* @since 4.9
*/
public boolean isDetachingSymbolicRef() {
return detachingSymbolicRef;
}
/**
* Set the new value the ref will update to.
*
* @param id
* the new value.
*/
public void setNewObjectId(AnyObjectId id) {
newValue = id.copy();
}
/**
* Get the expected value of the ref after the lock is taken, but before
* update occurs.
*
* @return the expected value of the ref after the lock is taken, but before
* update occurs. Null to avoid the compare and swap test. Use
* {@link org.eclipse.jgit.lib.ObjectId#zeroId()} to indicate
* expectation of a non-existant ref.
*/
public ObjectId getExpectedOldObjectId() {
return expValue;
}
/**
* Set the expected value of the ref after the lock is taken, but before
* update occurs.
*
* @param id
* the expected value of the ref after the lock is taken, but
* before update occurs. Null to avoid the compare and swap test.
* Use {@link org.eclipse.jgit.lib.ObjectId#zeroId()} to indicate
* expectation of a non-existant ref.
*/
public void setExpectedOldObjectId(AnyObjectId id) {
expValue = id != null ? id.toObjectId() : null;
}
/**
* Check if this update wants to forcefully change the ref.
*
* @return true if this update should ignore merge tests.
*/
public boolean isForceUpdate() {
return force;
}
/**
* Set if this update wants to forcefully change the ref.
*
* @param b
* true if this update should ignore merge tests.
*/
public void setForceUpdate(boolean b) {
force = b;
}
/**
* Get identity of the user making the change in the reflog.
*
* @return identity of the user making the change in the reflog.
*/
public PersonIdent getRefLogIdent() {
return refLogIdent;
}
/**
* Set the identity of the user appearing in the reflog.
* <p>
* The timestamp portion of the identity is ignored. A new identity with the
* current timestamp will be created automatically when the update occurs
* and the log record is written.
*
* @param pi
* identity of the user. If null the identity will be
* automatically determined based on the repository
* configuration.
*/
public void setRefLogIdent(PersonIdent pi) {
refLogIdent = pi;
}
/**
* Get the message to include in the reflog.
*
* @return message the caller wants to include in the reflog; null if the
* update should not be logged.
*/
public String getRefLogMessage() {
return refLogMessage;
}
/**
* Whether the ref log message should show the result.
*
* @return {@code true} if the ref log message should show the result.
*/
protected boolean isRefLogIncludingResult() {
return refLogIncludeResult;
}
/**
* Set the message to include in the reflog.
* <p>
* Repository implementations may limit which reflogs are written by default,
* based on the project configuration. If a repo is not configured to write
* logs for this ref by default, setting the message alone may have no effect.
* To indicate that the repo should write logs for this update in spite of
* configured defaults, use {@link #setForceRefLog(boolean)}.
*
* @param msg
* the message to describe this change. It may be null if
* appendStatus is null in order not to append to the reflog
* @param appendStatus
* true if the status of the ref change (fast-forward or
* forced-update) should be appended to the user supplied
* message.
*/
public void setRefLogMessage(String msg, boolean appendStatus) {
if (msg == null && !appendStatus)
disableRefLog();
else if (msg == null && appendStatus) {
refLogMessage = ""; //$NON-NLS-1$
refLogIncludeResult = true;
} else {
refLogMessage = msg;
refLogIncludeResult = appendStatus;
}
}
/**
* Don't record this update in the ref's associated reflog.
*/
public void disableRefLog() {
refLogMessage = null;
refLogIncludeResult = false;
}
/**
* Force writing a reflog for the updated ref.
*
* @param force whether to force.
* @since 4.9
*/
public void setForceRefLog(boolean force) {
forceRefLog = force;
}
/**
* Check whether the reflog should be written regardless of repo defaults.
*
* @return whether force writing is enabled.
* @since 4.9
*/
protected boolean isForceRefLog() {
return forceRefLog;
}
/**
* The old value of the ref, prior to the update being attempted.
* <p>
* This value may differ before and after the update method. Initially it is
* populated with the value of the ref before the lock is taken, but the old
* value may change if someone else modified the ref between the time we
* last read it and when the ref was locked for update.
*
* @return the value of the ref prior to the update being attempted; null if
* the updated has not been attempted yet.
*/
public ObjectId getOldObjectId() {
return oldValue;
}
/**
* Set the old value of the ref.
*
* @param old
* the old value.
*/
protected void setOldObjectId(ObjectId old) {
oldValue = old;
}
/**
* Set a push certificate associated with this update.
* <p>
* This usually includes a command to update this ref, but is not required to.
*
* @param cert
* push certificate, may be null.
* @since 4.1
*/
public void setPushCertificate(PushCertificate cert) {
pushCert = cert;
}
/**
* Set the push certificate associated with this update.
* <p>
* This usually includes a command to update this ref, but is not required to.
*
* @return push certificate, may be null.
* @since 4.1
*/
protected PushCertificate getPushCertificate() {
return pushCert;
}
/**
* Get the status of this update.
* <p>
* The same value that was previously returned from an update method.
*
* @return the status of the update.
*/
public Result getResult() {
return result;
}
private void requireCanDoUpdate() {
if (newValue == null)
throw new IllegalStateException(JGitText.get().aNewObjectIdIsRequired);
}
/**
* Force the ref to take the new value.
* <p>
* This is just a convenient helper for setting the force flag, and as such
* the merge test is performed.
*
* @return the result status of the update.
* @throws java.io.IOException
* an unexpected IO error occurred while writing changes.
*/
public Result forceUpdate() throws IOException {
force = true;
return update();
}
/**
* Gracefully update the ref to the new value.
* <p>
* Merge test will be performed according to {@link #isForceUpdate()}.
* <p>
* This is the same as:
*
* <pre>
* return update(new RevWalk(getRepository()));
* </pre>
*
* @return the result status of the update.
* @throws java.io.IOException
* an unexpected IO error occurred while writing changes.
*/
public Result update() throws IOException {
try (RevWalk rw = new RevWalk(getRepository())) {
rw.setRetainBody(false);
return update(rw);
}
}
/**
* Gracefully update the ref to the new value.
* <p>
* Merge test will be performed according to {@link #isForceUpdate()}.
*
* @param walk
* a RevWalk instance this update command can borrow to perform
* the merge test. The walk will be reset to perform the test.
* @return the result status of the update.
* @throws java.io.IOException
* an unexpected IO error occurred while writing changes.
*/
public Result update(RevWalk walk) throws IOException {
requireCanDoUpdate();
try {
return result = updateImpl(walk, new Store() {
@Override
Result execute(Result status) throws IOException {
if (status == Result.NO_CHANGE)
return status;
return doUpdate(status);
}
});
} catch (IOException x) {
result = Result.IO_FAILURE;
throw x;
}
}
/**
* Delete the ref.
* <p>
* This is the same as:
*
* <pre>
* return delete(new RevWalk(getRepository()));
* </pre>
*
* @return the result status of the delete.
* @throws java.io.IOException
*/
public Result delete() throws IOException {
try (RevWalk rw = new RevWalk(getRepository())) {
rw.setRetainBody(false);
return delete(rw);
}
}
/**
* Delete the ref.
*
* @param walk
* a RevWalk instance this delete command can borrow to perform
* the merge test. The walk will be reset to perform the test.
* @return the result status of the delete.
* @throws java.io.IOException
*/
public Result delete(RevWalk walk) throws IOException {
final String myName = detachingSymbolicRef
? getRef().getName()
: getRef().getLeaf().getName();
if (myName.startsWith(Constants.R_HEADS) && !getRepository().isBare()) {
// Don't allow the currently checked out branch to be deleted.
Ref head = getRefDatabase().exactRef(Constants.HEAD);
while (head != null && head.isSymbolic()) {
head = head.getTarget();
if (myName.equals(head.getName()))
return result = Result.REJECTED_CURRENT_BRANCH;
}
}
try {
return result = updateImpl(walk, new Store() {
@Override
Result execute(Result status) throws IOException {
return doDelete(status);
}
});
} catch (IOException x) {
result = Result.IO_FAILURE;
throw x;
}
}
/**
* Replace this reference with a symbolic reference to another reference.
* <p>
* This exact reference (not its traversed leaf) is replaced with a symbolic
* reference to the requested name.
*
* @param target
* name of the new target for this reference. The new target name
* must be absolute, so it must begin with {@code refs/}.
* @return {@link org.eclipse.jgit.lib.RefUpdate.Result#NEW} or
* {@link org.eclipse.jgit.lib.RefUpdate.Result#FORCED} on success.
* @throws java.io.IOException
*/
public Result link(String target) throws IOException {
if (!target.startsWith(Constants.R_REFS))
throw new IllegalArgumentException(MessageFormat.format(JGitText.get().illegalArgumentNotA, Constants.R_REFS));
if (checkConflicting && getRefDatabase().isNameConflicting(getName()))
return Result.LOCK_FAILURE;
try {
if (!tryLock(false))
return Result.LOCK_FAILURE;
final Ref old = getRefDatabase().exactRef(getName());
if (old != null && old.isSymbolic()) {
final Ref dst = old.getTarget();
if (target.equals(dst.getName()))
return result = Result.NO_CHANGE;
}
if (old != null && old.getObjectId() != null)
setOldObjectId(old.getObjectId());
final Ref dst = getRefDatabase().exactRef(target);
if (dst != null && dst.getObjectId() != null)
setNewObjectId(dst.getObjectId());
return result = doLink(target);
} catch (IOException x) {
result = Result.IO_FAILURE;
throw x;
} finally {
unlock();
}
}
private Result updateImpl(RevWalk walk, Store store)
throws IOException {
RevObject newObj;
RevObject oldObj;
// don't make expensive conflict check if this is an existing Ref
if (oldValue == null && checkConflicting
&& getRefDatabase().isNameConflicting(getName())) {
return Result.LOCK_FAILURE;
}
try {
// If we're detaching a symbolic reference, we should update the reference
// itself. Otherwise, we will update the leaf reference, which should be
// an ObjectIdRef.
if (!tryLock(!detachingSymbolicRef)) {
return Result.LOCK_FAILURE;
}
if (expValue != null) {
final ObjectId o;
o = oldValue != null ? oldValue : ObjectId.zeroId();
if (!AnyObjectId.isEqual(expValue, o)) {
return Result.LOCK_FAILURE;
}
}
try {
newObj = safeParseNew(walk, newValue);
} catch (MissingObjectException e) {
return Result.REJECTED_MISSING_OBJECT;
}
if (oldValue == null) {
return store.execute(Result.NEW);
}
oldObj = safeParseOld(walk, oldValue);
if (References.isSameObject(newObj, oldObj)
&& !detachingSymbolicRef) {
return store.execute(Result.NO_CHANGE);
}
if (isForceUpdate()) {
return store.execute(Result.FORCED);
}
if (newObj instanceof RevCommit && oldObj instanceof RevCommit) {
if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj)) {
return store.execute(Result.FAST_FORWARD);
}
}
return Result.REJECTED;
} finally {
unlock();
}
}
/**
* Enable/disable the check for conflicting ref names. By default conflicts
* are checked explicitly.
*
* @param check
* whether to enable the check for conflicting ref names.
* @since 3.0
*/
public void setCheckConflicting(boolean check) {
checkConflicting = check;
}
private static RevObject safeParseNew(RevWalk rw, AnyObjectId newId)
throws IOException {
if (newId == null || ObjectId.zeroId().equals(newId)) {
return null;
}
return rw.parseAny(newId);
}
private static RevObject safeParseOld(RevWalk rw, AnyObjectId oldId)
throws IOException {
try {
return oldId != null ? rw.parseAny(oldId) : null;
} catch (MissingObjectException e) {
// We can expect some old objects to be missing, like if we are trying to
// force a deletion of a branch and the object it points to has been
// pruned from the database due to freak corruption accidents (it happens
// with 'git new-work-dir').
return null;
}
}
/**
* Handle the abstraction of storing a ref update. This is because both
* updating and deleting of a ref have merge testing in common.
*/
private abstract static class Store {
abstract Result execute(Result status) throws IOException;
}
}