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: }