tikhomirov@231: /* tikhomirov@415: * Copyright (c) 2011-2012 TMate Software Ltd tikhomirov@231: * tikhomirov@231: * This program is free software; you can redistribute it and/or modify tikhomirov@231: * it under the terms of the GNU General Public License as published by tikhomirov@231: * the Free Software Foundation; version 2 of the License. tikhomirov@231: * tikhomirov@231: * This program is distributed in the hope that it will be useful, tikhomirov@231: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@231: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@231: * GNU General Public License for more details. tikhomirov@231: * tikhomirov@231: * For information on how to redistribute this software under tikhomirov@231: * the terms of a license other than GNU General Public License tikhomirov@231: * contact TMate Software at support@hg4j.com tikhomirov@231: */ tikhomirov@231: package org.tmatesoft.hg.repo; tikhomirov@231: tikhomirov@270: import static org.tmatesoft.hg.core.Nodeid.NULL; tikhomirov@270: tikhomirov@231: import java.io.BufferedReader; tikhomirov@231: import java.io.File; tikhomirov@231: import java.io.FileReader; tikhomirov@231: import java.io.IOException; tikhomirov@231: import java.util.ArrayList; tikhomirov@231: import java.util.Arrays; tikhomirov@231: import java.util.Collections; tikhomirov@231: import java.util.List; tikhomirov@231: tikhomirov@231: import org.tmatesoft.hg.core.HgFileRevision; tikhomirov@231: import org.tmatesoft.hg.core.Nodeid; tikhomirov@490: import org.tmatesoft.hg.internal.Internals; tikhomirov@248: import org.tmatesoft.hg.internal.ManifestRevision; tikhomirov@231: import org.tmatesoft.hg.internal.Pool; tikhomirov@284: import org.tmatesoft.hg.util.Pair; tikhomirov@231: import org.tmatesoft.hg.util.Path; tikhomirov@231: import org.tmatesoft.hg.util.PathRewrite; tikhomirov@231: tikhomirov@231: /** tikhomirov@423: * Access to repository's merge state tikhomirov@423: * tikhomirov@231: * @author Artem Tikhomirov tikhomirov@231: * @author TMate Software Ltd. tikhomirov@231: */ tikhomirov@231: public class HgMergeState { tikhomirov@270: private Nodeid wcp1, wcp2, stateParent; tikhomirov@231: tikhomirov@231: public enum Kind { tikhomirov@231: Resolved, Unresolved; tikhomirov@231: } tikhomirov@231: tikhomirov@231: public static class Entry { tikhomirov@231: private final Kind state; tikhomirov@231: private final HgFileRevision parent1; tikhomirov@231: private final HgFileRevision parent2; tikhomirov@231: private final HgFileRevision ancestor; tikhomirov@231: private final Path wcFile; tikhomirov@231: tikhomirov@231: /*package-local*/Entry(Kind s, Path actualCopy, HgFileRevision p1, HgFileRevision p2, HgFileRevision ca) { tikhomirov@231: if (p1 == null || p2 == null || ca == null || actualCopy == null) { tikhomirov@231: throw new IllegalArgumentException(); tikhomirov@231: } tikhomirov@231: state = s; tikhomirov@231: wcFile = actualCopy; tikhomirov@231: parent1 = p1; tikhomirov@231: parent2 = p2; tikhomirov@231: ancestor = ca; tikhomirov@231: } tikhomirov@231: tikhomirov@231: public Kind getState() { tikhomirov@231: return state; tikhomirov@231: } tikhomirov@231: public Path getActualFile() { tikhomirov@231: return wcFile; tikhomirov@231: } tikhomirov@231: public HgFileRevision getFirstParent() { tikhomirov@231: return parent1; tikhomirov@231: } tikhomirov@231: public HgFileRevision getSecondParent() { tikhomirov@231: return parent2; tikhomirov@231: } tikhomirov@231: public HgFileRevision getCommonAncestor() { tikhomirov@231: return ancestor; tikhomirov@231: } tikhomirov@231: } tikhomirov@231: tikhomirov@490: private final Internals repo; tikhomirov@231: private Entry[] entries; tikhomirov@231: tikhomirov@490: HgMergeState(Internals internalRepo) { tikhomirov@490: repo = internalRepo; tikhomirov@231: } tikhomirov@231: tikhomirov@423: /** tikhomirov@423: * Update our knowledge about repository's merge state tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: */ tikhomirov@423: public void refresh() throws HgRuntimeException { tikhomirov@490: final HgRepository hgRepo = repo.getRepo(); tikhomirov@231: entries = null; tikhomirov@341: // it's possible there are two parents but no merge/state, we shall report this case as 'merging', with proper tikhomirov@341: // first and second parent values tikhomirov@341: stateParent = Nodeid.NULL; tikhomirov@341: Pool nodeidPool = new Pool(); tikhomirov@341: Pool fnamePool = new Pool(); tikhomirov@490: Pair wcParents = hgRepo.getWorkingCopyParents(); tikhomirov@341: wcp1 = nodeidPool.unify(wcParents.first()); wcp2 = nodeidPool.unify(wcParents.second()); tikhomirov@490: final File f = repo.getFileFromRepoDir("merge/state"); tikhomirov@231: if (!f.canRead()) { tikhomirov@231: // empty state tikhomirov@231: return; tikhomirov@231: } tikhomirov@348: try { tikhomirov@348: ArrayList result = new ArrayList(); tikhomirov@431: // pipe (already normalized) names from mergestate through same pool of filenames as use manifest revisions tikhomirov@431: Path.Source pathPool = new Path.SimpleSource(new PathRewrite.Empty(), fnamePool); tikhomirov@348: final ManifestRevision m1 = new ManifestRevision(nodeidPool, fnamePool); tikhomirov@348: final ManifestRevision m2 = new ManifestRevision(nodeidPool, fnamePool); tikhomirov@348: if (!wcp2.isNull()) { tikhomirov@490: final int rp2 = hgRepo.getChangelog().getRevisionIndex(wcp2); tikhomirov@490: hgRepo.getManifest().walk(rp2, rp2, m2); tikhomirov@270: } tikhomirov@348: BufferedReader br = new BufferedReader(new FileReader(f)); tikhomirov@348: String s = br.readLine(); tikhomirov@348: stateParent = nodeidPool.unify(Nodeid.fromAscii(s)); tikhomirov@490: final int rp1 = hgRepo.getChangelog().getRevisionIndex(stateParent); tikhomirov@490: hgRepo.getManifest().walk(rp1, rp1, m1); tikhomirov@348: while ((s = br.readLine()) != null) { tikhomirov@348: String[] r = s.split("\\00"); tikhomirov@348: Path p1fname = pathPool.path(r[3]); tikhomirov@348: Nodeid nidP1 = m1.nodeid(p1fname); tikhomirov@348: Nodeid nidCA = nodeidPool.unify(Nodeid.fromAscii(r[5])); tikhomirov@490: HgFileRevision p1 = new HgFileRevision(hgRepo, nidP1, m1.flags(p1fname), p1fname); tikhomirov@348: HgFileRevision ca; tikhomirov@348: if (nidCA == nidP1 && r[3].equals(r[4])) { tikhomirov@348: ca = p1; tikhomirov@348: } else { tikhomirov@490: ca = new HgFileRevision(hgRepo, nidCA, null, pathPool.path(r[4])); tikhomirov@270: } tikhomirov@348: HgFileRevision p2; tikhomirov@348: if (!wcp2.isNull() || !r[6].equals(r[4])) { tikhomirov@348: final Path p2fname = pathPool.path(r[6]); tikhomirov@348: Nodeid nidP2 = m2.nodeid(p2fname); tikhomirov@348: if (nidP2 == null) { tikhomirov@348: assert false : "There's not enough information (or I don't know where to look) in merge/state to find out what's the second parent"; tikhomirov@348: nidP2 = NULL; tikhomirov@348: } tikhomirov@490: p2 = new HgFileRevision(hgRepo, nidP2, m2.flags(p2fname), p2fname); tikhomirov@348: } else { tikhomirov@348: // no second parent known. no idea what to do here, assume linear merge, use common ancestor as parent tikhomirov@348: p2 = ca; tikhomirov@348: } tikhomirov@348: final Kind k; tikhomirov@348: if ("u".equals(r[1])) { tikhomirov@348: k = Kind.Unresolved; tikhomirov@348: } else if ("r".equals(r[1])) { tikhomirov@348: k = Kind.Resolved; tikhomirov@348: } else { tikhomirov@423: throw new HgInvalidStateException(String.format("Unknown merge kind %s", r[1])); tikhomirov@348: } tikhomirov@348: Entry e = new Entry(k, pathPool.path(r[0]), p1, p2, ca); tikhomirov@348: result.add(e); tikhomirov@270: } tikhomirov@348: entries = result.toArray(new Entry[result.size()]); tikhomirov@348: br.close(); tikhomirov@348: } catch (IOException ex) { tikhomirov@348: throw new HgInvalidControlFileException("Merge state read failed", ex, f); tikhomirov@231: } tikhomirov@231: } tikhomirov@270: tikhomirov@341: /** tikhomirov@341: * Repository is in 'merging' state when changeset to be committed got two parents. tikhomirov@341: * This method doesn't tell whether there are (un)resolved conflicts in the working copy, tikhomirov@341: * use {@link #getConflicts()} (which makes sense only when {@link #isStale()} is false). tikhomirov@341: * @return true when repository is being merged tikhomirov@341: */ tikhomirov@270: public boolean isMerging() { tikhomirov@270: return !getFirstParent().isNull() && !getSecondParent().isNull() && !isStale(); tikhomirov@270: } tikhomirov@270: tikhomirov@270: /** tikhomirov@341: * Merge state file may not match actual working copy due to rollback or undo operations. tikhomirov@341: * Value of {@link #getConflicts()} is reasonable iff this method returned false. tikhomirov@341: * tikhomirov@270: * @return true when recorded merge state doesn't seem to correspond to present working copy tikhomirov@270: */ tikhomirov@270: public boolean isStale() { tikhomirov@270: if (wcp1 == null) { tikhomirov@423: refresh(); tikhomirov@270: } tikhomirov@341: return !stateParent.isNull() /*there's merge state*/ && !wcp1.equals(stateParent) /*and it doesn't match*/; tikhomirov@270: } tikhomirov@336: tikhomirov@336: /** tikhomirov@341: * It's possible for a repository to be in a 'merging' state (@see {@link #isMerging()} without any tikhomirov@341: * conflict to resolve (no merge state information file). tikhomirov@423: * tikhomirov@341: * @return first parent of the working copy, never null tikhomirov@336: */ tikhomirov@231: public Nodeid getFirstParent() { tikhomirov@231: if (wcp1 == null) { tikhomirov@423: refresh(); tikhomirov@231: } tikhomirov@231: return wcp1; tikhomirov@231: } tikhomirov@231: tikhomirov@341: /** tikhomirov@341: * @return second parent of the working copy, never null tikhomirov@341: */ tikhomirov@231: public Nodeid getSecondParent() { tikhomirov@231: if (wcp2 == null) { tikhomirov@423: refresh(); tikhomirov@231: } tikhomirov@231: return wcp2; tikhomirov@231: } tikhomirov@231: tikhomirov@336: /** tikhomirov@336: * @return revision of the merge state or {@link Nodeid#NULL} if there's no merge state tikhomirov@336: */ tikhomirov@270: public Nodeid getStateParent() { tikhomirov@270: if (stateParent == null) { tikhomirov@423: refresh(); tikhomirov@270: } tikhomirov@270: return stateParent; tikhomirov@270: } tikhomirov@270: tikhomirov@341: /** tikhomirov@341: * List of conflicts as recorded in the merge state information file. tikhomirov@341: * Note, this information is valid unless {@link #isStale()} is true. tikhomirov@341: * tikhomirov@341: * @return non-null list with both resolved and unresolved conflicts. tikhomirov@341: */ tikhomirov@231: public List getConflicts() { tikhomirov@231: return entries == null ? Collections.emptyList() : Arrays.asList(entries); tikhomirov@231: } tikhomirov@231: }