| // Copyright (C) 2011 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License.package com.google.gerrit.server.git; |
| |
| package com.google.gerrit.server.git; |
| |
| import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull; |
| import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull; |
| |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gerrit.reviewdb.client.Project; |
| |
| import org.eclipse.jgit.errors.IncorrectObjectTypeException; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectIdOwnerMap; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.revwalk.RevCommit; |
| import org.eclipse.jgit.revwalk.RevSort; |
| import org.eclipse.jgit.revwalk.RevWalk; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.util.BitSet; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| class TagSet { |
| private static final Logger log = LoggerFactory.getLogger(TagSet.class); |
| |
| private final Project.NameKey projectName; |
| private final Map<String, CachedRef> refs; |
| private final ObjectIdOwnerMap<Tag> tags; |
| |
| TagSet(Project.NameKey projectName) { |
| this.projectName = projectName; |
| this.refs = new HashMap<String, CachedRef>(); |
| this.tags = new ObjectIdOwnerMap<Tag>(); |
| } |
| |
| Tag lookupTag(AnyObjectId id) { |
| return tags.get(id); |
| } |
| |
| boolean updateFastForward(String refName, ObjectId oldValue, |
| ObjectId newValue) { |
| CachedRef ref = refs.get(refName); |
| if (ref != null) { |
| // compareAndSet works on reference equality, but this operation |
| // wants to use object equality. Switch out oldValue with cur so the |
| // compareAndSet will function correctly for this operation. |
| // |
| ObjectId cur = ref.get(); |
| if (cur.equals(oldValue)) { |
| return ref.compareAndSet(cur, newValue); |
| } |
| } |
| return false; |
| } |
| |
| void prepare(TagMatcher m) { |
| RevWalk rw = null; |
| try { |
| for (Ref currentRef : m.include) { |
| if (currentRef.isSymbolic()) { |
| continue; |
| } |
| if (currentRef.getObjectId() == null) { |
| continue; |
| } |
| |
| CachedRef savedRef = refs.get(currentRef.getName()); |
| if (savedRef == null) { |
| // If the reference isn't known to the set, return null |
| // and force the caller to rebuild the set in a new copy. |
| m.newRefs.add(currentRef); |
| continue; |
| } |
| |
| // The reference has not been moved. It can be used as-is. |
| ObjectId savedObjectId = savedRef.get(); |
| if (currentRef.getObjectId().equals(savedObjectId)) { |
| m.mask.set(savedRef.flag); |
| continue; |
| } |
| |
| // Check on-the-fly to see if the branch still reaches the tag. |
| // This is very likely for a branch that fast-forwarded. |
| try { |
| if (rw == null) { |
| rw = new RevWalk(m.db); |
| rw.setRetainBody(false); |
| } |
| |
| RevCommit savedCommit = rw.parseCommit(savedObjectId); |
| RevCommit currentCommit = rw.parseCommit(currentRef.getObjectId()); |
| if (rw.isMergedInto(savedCommit, currentCommit)) { |
| // Fast-forward. Safely update the reference in-place. |
| savedRef.compareAndSet(savedObjectId, currentRef.getObjectId()); |
| m.mask.set(savedRef.flag); |
| continue; |
| } |
| |
| // The branch rewound. Walk the list of commits removed from |
| // the reference. If any matches to a tag, this has to be removed. |
| boolean err = false; |
| rw.reset(); |
| rw.markStart(savedCommit); |
| rw.markUninteresting(currentCommit); |
| rw.sort(RevSort.TOPO, true); |
| RevCommit c; |
| while ((c = rw.next()) != null) { |
| Tag tag = tags.get(c); |
| if (tag != null && tag.refFlags.get(savedRef.flag)) { |
| m.lostRefs.add(new TagMatcher.LostRef(tag, savedRef.flag)); |
| err = true; |
| } |
| } |
| if (!err) { |
| // All of the tags are still reachable. Update in-place. |
| savedRef.compareAndSet(savedObjectId, currentRef.getObjectId()); |
| m.mask.set(savedRef.flag); |
| } |
| |
| } catch (IOException err) { |
| // Defer a cache update until later. No conclusion can be made |
| // based on an exception reading from the repository storage. |
| log.warn("Error checking tags of " + projectName, err); |
| } |
| } |
| } finally { |
| if (rw != null) { |
| rw.release(); |
| } |
| } |
| } |
| |
| void build(Repository git, TagSet old, TagMatcher m) { |
| if (old != null && m != null && refresh(old, m)) { |
| return; |
| } |
| |
| TagWalk rw = new TagWalk(git); |
| rw.setRetainBody(false); |
| try { |
| for (Ref ref : git.getAllRefs().values()) { |
| if (skip(ref)) { |
| continue; |
| |
| } else if (isTag(ref)) { |
| // For a tag, remember where it points to. |
| addTag(rw, git.peel(ref)); |
| |
| } else { |
| // New reference to include in the set. |
| addRef(rw, ref); |
| } |
| } |
| |
| // Traverse the complete history. Copy any flags from a commit to |
| // all of its ancestors. This automatically updates any Tag object |
| // as the TagCommit and the stored Tag object share the same |
| // underlying bit set. |
| TagCommit c; |
| while ((c = (TagCommit) rw.next()) != null) { |
| BitSet mine = c.refFlags; |
| int pCnt = c.getParentCount(); |
| for (int pIdx = 0; pIdx < pCnt; pIdx++) { |
| ((TagCommit) c.getParent(pIdx)).refFlags.or(mine); |
| } |
| } |
| } catch (IOException e) { |
| log.warn("Repository " + projectName + " has corruption", e); |
| } finally { |
| rw.release(); |
| } |
| } |
| |
| void readObject(ObjectInputStream in) throws IOException, |
| ClassNotFoundException { |
| int refCnt = in.readInt(); |
| for (int i = 0; i < refCnt; i++) { |
| String name = in.readUTF(); |
| int flag = in.readInt(); |
| ObjectId id = readNotNull(in); |
| refs.put(name, new CachedRef(flag, id)); |
| } |
| |
| int tagCnt = in.readInt(); |
| for (int i = 0; i < tagCnt; i++) { |
| ObjectId id = readNotNull(in); |
| BitSet flags = (BitSet) in.readObject(); |
| tags.add(new Tag(id, flags)); |
| } |
| } |
| |
| void writeObject(ObjectOutputStream out) throws IOException { |
| out.writeInt(refs.size()); |
| for (Map.Entry<String, CachedRef> e : refs.entrySet()) { |
| out.writeUTF(e.getKey()); |
| out.writeInt(e.getValue().flag); |
| writeNotNull(out, e.getValue().get()); |
| } |
| |
| out.writeInt(tags.size()); |
| for (Tag tag : tags) { |
| writeNotNull(out, tag); |
| out.writeObject(tag.refFlags); |
| } |
| } |
| |
| private boolean refresh(TagSet old, TagMatcher m) { |
| if (m.newRefs.isEmpty()) { |
| // No new references is a simple update. Copy from the old set. |
| copy(old, m); |
| return true; |
| } |
| |
| // Only permit a refresh if all new references start from the tip of |
| // an existing references. This happens some of the time within a |
| // Gerrit Code Review server, perhaps about 50% of new references. |
| // Since a complete rebuild is so costly, try this approach first. |
| |
| Map<ObjectId, Integer> byObj = new HashMap<ObjectId, Integer>(); |
| for (CachedRef r : old.refs.values()) { |
| ObjectId id = r.get(); |
| if (!byObj.containsKey(id)) { |
| byObj.put(id, r.flag); |
| } |
| } |
| |
| for (Ref newRef : m.newRefs) { |
| ObjectId id = newRef.getObjectId(); |
| if (id == null || refs.containsKey(newRef.getName())) { |
| continue; |
| } else if (!byObj.containsKey(id)) { |
| return false; |
| } |
| } |
| |
| copy(old, m); |
| |
| for (Ref newRef : m.newRefs) { |
| ObjectId id = newRef.getObjectId(); |
| if (id == null || refs.containsKey(newRef.getName())) { |
| continue; |
| } |
| |
| int srcFlag = byObj.get(id); |
| int newFlag = refs.size(); |
| refs.put(newRef.getName(), new CachedRef(newRef, newFlag)); |
| |
| for (Tag tag : tags) { |
| if (tag.refFlags.get(srcFlag)) { |
| tag.refFlags.set(newFlag); |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| private void copy(TagSet old, TagMatcher m) { |
| refs.putAll(old.refs); |
| |
| for (Tag srcTag : old.tags) { |
| BitSet mine = new BitSet(); |
| mine.or(srcTag.refFlags); |
| tags.add(new Tag(srcTag, mine)); |
| } |
| |
| for (TagMatcher.LostRef lost : m.lostRefs) { |
| Tag mine = tags.get(lost.tag); |
| if (mine != null) { |
| mine.refFlags.clear(lost.flag); |
| } |
| } |
| } |
| |
| private void addTag(TagWalk rw, Ref ref) { |
| ObjectId id = ref.getPeeledObjectId(); |
| if (id == null) { |
| id = ref.getObjectId(); |
| } |
| |
| if (!tags.contains(id)) { |
| BitSet flags; |
| try { |
| flags = ((TagCommit) rw.parseCommit(id)).refFlags; |
| } catch (IncorrectObjectTypeException notCommit) { |
| flags = new BitSet(); |
| } catch (IOException e) { |
| log.warn("Error on " + ref.getName() + " of " + projectName, e); |
| flags = new BitSet(); |
| } |
| tags.add(new Tag(id, flags)); |
| } |
| } |
| |
| private void addRef(TagWalk rw, Ref ref) { |
| try { |
| TagCommit commit = (TagCommit) rw.parseCommit(ref.getObjectId()); |
| rw.markStart(commit); |
| |
| int flag = refs.size(); |
| commit.refFlags.set(flag); |
| refs.put(ref.getName(), new CachedRef(ref, flag)); |
| } catch (IncorrectObjectTypeException notCommit) { |
| // No need to spam the logs. |
| // Quite many refs will point to non-commits. |
| // For instance, refs from refs/cache-automerge |
| // will often end up here. |
| } catch (IOException e) { |
| log.warn("Error on " + ref.getName() + " of " + projectName, e); |
| } |
| } |
| |
| private static boolean skip(Ref ref) { |
| return ref.isSymbolic() || ref.getObjectId() == null |
| || PatchSet.isRef(ref.getName()); |
| } |
| |
| private static boolean isTag(Ref ref) { |
| return ref.getName().startsWith(Constants.R_TAGS); |
| } |
| |
| static final class Tag extends ObjectIdOwnerMap.Entry { |
| private final BitSet refFlags; |
| |
| Tag(AnyObjectId id, BitSet flags) { |
| super(id); |
| this.refFlags = flags; |
| } |
| |
| boolean has(BitSet mask) { |
| return refFlags.intersects(mask); |
| } |
| } |
| |
| private static final class CachedRef extends AtomicReference<ObjectId> { |
| private static final long serialVersionUID = 1L; |
| |
| final int flag; |
| |
| CachedRef(Ref ref, int flag) { |
| this(flag, ref.getObjectId()); |
| } |
| |
| CachedRef(int flag, ObjectId id) { |
| this.flag = flag; |
| set(id); |
| } |
| } |
| |
| private static final class TagWalk extends RevWalk { |
| TagWalk(Repository git) { |
| super(git); |
| } |
| |
| @Override |
| protected TagCommit createCommit(AnyObjectId id) { |
| return new TagCommit(id); |
| } |
| } |
| |
| private static final class TagCommit extends RevCommit { |
| final BitSet refFlags; |
| |
| TagCommit(AnyObjectId id) { |
| super(id); |
| refFlags = new BitSet(); |
| } |
| } |
| } |