view src/org/tmatesoft/hg/core/HgChangeset.java @ 709:497e697636fc

Report merged lines as changed block if possible, not as a sequence of added/deleted blocks. To facilitate access to merge parent lines AddBlock got mergeLineAt() method that reports index of the line in the second parent (if any), while insertedAt() has been changed to report index in the first parent always
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 21 Aug 2013 16:23:27 +0200
parents 934037edbea0
children
line wrap: on
line source
/*
 * Copyright (c) 2011-2012 TMate Software Ltd
 *  
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * For information on how to redistribute this software under
 * the terms of a license other than GNU General Public License
 * contact TMate Software at support@hg4j.com
 */
package org.tmatesoft.hg.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.tmatesoft.hg.internal.PhasesHelper;
import org.tmatesoft.hg.repo.HgChangelog;
import org.tmatesoft.hg.repo.HgChangelog.RawChangeset;
import org.tmatesoft.hg.repo.HgInternals;
import org.tmatesoft.hg.repo.HgPhase;
import org.tmatesoft.hg.repo.HgInvalidStateException;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgRuntimeException;
import org.tmatesoft.hg.repo.HgStatusCollector;
import org.tmatesoft.hg.repo.HgParentChildMap;
import org.tmatesoft.hg.util.CancelledException;
import org.tmatesoft.hg.util.Path;


/**
 * Record in the Mercurial changelog, describing single commit.
 * 
 * Not thread-safe, don't try to read from different threads
 * 
 * @author Artem Tikhomirov
 * @author TMate Software Ltd.
 */
public class HgChangeset implements Cloneable {

	// these get initialized
	private RawChangeset changeset;
	private int revNumber;
	private Nodeid nodeid;

	class ShareDataStruct {
		ShareDataStruct(HgStatusCollector statusCollector, Path.Source pathFactory) {
			statusHelper = statusCollector;
			pathHelper = pathFactory;
		}
		public final HgStatusCollector statusHelper;
		public final Path.Source pathHelper;

		public HgParentChildMap<HgChangelog> parentHelper;
		public PhasesHelper phaseHelper;
	};

	// Helpers/utilities shared among few instances of HgChangeset
	private final ShareDataStruct shared;

	// these are built on demand
	private List<HgFileRevision> modifiedFiles, addedFiles;
	private List<Path> deletedFiles;
	private byte[] parent1, parent2;
	

	// XXX consider CommandContext with StatusCollector, PathPool etc. Commands optionally get CC through a cons or create new
	// and pass it around
	/*package-local*/HgChangeset(HgStatusCollector statusCollector, Path.Source pathFactory) {
		shared = new ShareDataStruct(statusCollector, pathFactory);
	}

	/*package-local*/ void init(int localRevNumber, Nodeid nid, RawChangeset rawChangeset) {
		revNumber = localRevNumber;
		nodeid = nid;
		changeset = rawChangeset.clone();
		modifiedFiles = addedFiles = null;
		deletedFiles = null;
		parent1 = parent2 = null;
		// keep references to shared (and everything in there: parentHelper, statusHelper, phaseHelper and pathHelper)
	}

	/*package-local*/ void setParentHelper(HgParentChildMap<HgChangelog> pw) {
		if (pw != null) {
			if (pw.getRepo() != shared.statusHelper.getRepo()) {
				throw new IllegalArgumentException();
			}
		}
		shared.parentHelper = pw;
	}

	/**
	 * Index of the changeset in local repository. Note, this number is relevant only for local repositories/operations, use 
	 * {@link Nodeid nodeid} to uniquely identify a revision.
	 *   
	 * @return index of the changeset revision
	 */
	public int getRevisionIndex() {
		return revNumber;
	}

	/**
	 * Unique identity of this changeset revision
	 * @return revision identifier, never <code>null</code>
	 */
	public Nodeid getNodeid() {
		return nodeid;
	}

	/**
	 * Name of the user who made this commit
	 * @return author of the commit, never <code>null</code>
	 */
	public String getUser() {
		return changeset.user();
	}
	
	/**
	 * Commit description
	 * @return content of the corresponding field in changeset record; empty string if none specified.
	 */
	public String getComment() {
		return changeset.comment();
	}

	/**
	 * Name of the branch this commit was made in. Returns "default" for main branch.
	 * @return name of the branch, non-empty string
	 */
	public String getBranch() {
		return changeset.branch();
	}

	/**
	 * @return used to be String, now {@link HgDate}, use {@link HgDate#toString()} to get same result as before 
	 */
	public HgDate getDate() {
		return new HgDate(changeset.date().getTime(), changeset.timezone());
	}

	/**
	 * Indicates revision of manifest that tracks state of repository at the moment of this commit.
	 * Note, may be {@link Nodeid#NULL} in certain scenarios (e.g. first changeset in an empty repository, usually by bogus tools)
	 *  
	 * @return revision identifier, never <code>null</code>
	 */
	public Nodeid getManifestRevision() {
		return changeset.manifest();
	}

	/**
	 * Lists names of files affected by this commit, as recorded in the changeset itself. Unlike {@link #getAddedFiles()}, 
	 * {@link #getModifiedFiles()} and {@link #getRemovedFiles()}, this method doesn't analyze actual changes done 
	 * in the commit, rather extracts value from the changeset record.
	 * 
	 * List returned by this method may be empty, while aforementioned methods may produce non-empty result.
	 *   
	 * @return list of filenames, never <code>null</code>
	 */
	public List<Path> getAffectedFiles() {
		// reports files as recorded in changelog. Note, merge revisions may have no
		// files listed, and thus this method would return empty list, while
		// #getModifiedFiles() would return list with merged file(s) (because it uses status to get 'em, not
		// what #files() gives).
		ArrayList<Path> rv = new ArrayList<Path>(changeset.files().size());
		for (String name : changeset.files()) {
			rv.add(shared.pathHelper.path(name));
		}
		return rv;
	}

	/**
	 * Figures out files and specific revisions thereof that were modified in this commit
	 *  
	 * @return revisions of files modified in this commit
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public List<HgFileRevision> getModifiedFiles() throws HgRuntimeException {
		if (modifiedFiles == null) {
			initFileChanges();
		}
		return modifiedFiles;
	}

	/**
	 * Figures out files added in this commit
	 * 
	 * @return revisions of files added in this commit
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public List<HgFileRevision> getAddedFiles() throws HgRuntimeException {
		if (addedFiles == null) {
			initFileChanges();
		}
		return addedFiles;
	}

	/**
	 * Figures out files that were deleted as part of this commit
	 * 
	 * @return revisions of files deleted in this commit
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public List<Path> getRemovedFiles() throws HgRuntimeException {
		if (deletedFiles == null) {
			initFileChanges();
		}
		return deletedFiles;
	}

	public boolean isMerge() throws HgRuntimeException {
		// p1 == -1 and p2 != -1 is legitimate case
		return !(getFirstParentRevision().isNull() || getSecondParentRevision().isNull()); 
	}
	
	/**
	 * @return never <code>null</code>
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public Nodeid getFirstParentRevision() throws HgRuntimeException {
		if (shared.parentHelper != null) {
			return shared.parentHelper.safeFirstParent(nodeid);
		}
		// read once for both p1 and p2
		if (parent1 == null) {
			parent1 = new byte[20];
			parent2 = new byte[20];
			getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2);
		}
		return Nodeid.fromBinary(parent1, 0);
	}
	
	/**
	 * @return never <code>null</code>
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public Nodeid getSecondParentRevision() throws HgRuntimeException {
		if (shared.parentHelper != null) {
			return shared.parentHelper.safeSecondParent(nodeid);
		}
		if (parent2 == null) {
			parent1 = new byte[20];
			parent2 = new byte[20];
			getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2);
		}
		return Nodeid.fromBinary(parent2, 0);
	}

	/**	
	 * Tells the phase this changeset belongs to.
	 * @return one of {@link HgPhase} values
	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
	 */
	public HgPhase getPhase() throws HgRuntimeException {
		if (shared.phaseHelper == null) {
			// XXX would be handy to obtain ProgressSupport (perhaps, from statusHelper?)
			// and pass it to #init(), so that  there could be indication of file being read and cache being built
			synchronized (shared) {
				// ensure field is initialized only once 
				if (shared.phaseHelper == null) {
					shared.phaseHelper = new PhasesHelper(HgInternals.getImplementationRepo(getRepo()), shared.parentHelper);
				}
			}
		}
		return shared.phaseHelper.getPhase(this);
	}

	/**
	 * Create a copy of this changeset 
	 */
	@Override
	public HgChangeset clone() {
		try {
			HgChangeset copy = (HgChangeset) super.clone();
			// copy.changeset references this.changeset, doesn't need own copy
			return copy;
		} catch (CloneNotSupportedException ex) {
			throw new InternalError(ex.toString());
		}
	}
	
	@Override
	public String toString() {
		return String.format("HgChangeset(%d:%s)", revNumber, nodeid.shortNotation());

	}
	
	private HgRepository getRepo() {
		return shared.statusHelper.getRepo();
	}

	private /*synchronized*/ void initFileChanges() throws HgRuntimeException {
		ArrayList<Path> deleted = new ArrayList<Path>();
		ArrayList<HgFileRevision> modified = new ArrayList<HgFileRevision>();
		ArrayList<HgFileRevision> added = new ArrayList<HgFileRevision>();
		HgStatusCollector.Record r = new HgStatusCollector.Record();
		try {
			shared.statusHelper.change(revNumber, r);
		} catch (CancelledException ex) {
			// Record can't cancel
			throw new HgInvalidStateException("Internal error");
		}
		final HgRepository repo = getRepo();
		for (Path s : r.getModified()) {
			Nodeid nid = r.nodeidAfterChange(s);
			if (nid == null) {
				throw new HgInvalidStateException(String.format("For the file %s recorded as modified in changeset %d couldn't find revision after change", s, revNumber));
			}
			modified.add(new HgFileRevision(repo, nid, null, s, null));
		}
		final Map<Path, Path> copied = r.getCopied();
		for (Path s : r.getAdded()) {
			Nodeid nid = r.nodeidAfterChange(s);
			if (nid == null) {
				throw new HgInvalidStateException(String.format("For the file %s recorded as added in changeset %d couldn't find revision after change", s, revNumber));
			}
			added.add(new HgFileRevision(repo, nid, null, s, copied.get(s)));
		}
		for (Path s : r.getRemoved()) {
			// with Path from getRemoved, may just copy
			deleted.add(s);
		}
		modified.trimToSize();
		added.trimToSize();
		deleted.trimToSize();
		modifiedFiles = Collections.unmodifiableList(modified);
		addedFiles = Collections.unmodifiableList(added);
		deletedFiles = Collections.unmodifiableList(deleted);
	}
}