blob: fab661a99a322b1eac47cba882fb9b2987fa7584 [file] [log] [blame]
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* 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 org.zmlx.hg4idea.repo;
import com.intellij.dvcs.repo.RepoStateException;
import com.intellij.dvcs.repo.Repository;
import com.intellij.dvcs.repo.RepositoryUtil;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.vcs.log.Hash;
import com.intellij.vcs.log.VcsLogObjectsFactory;
import org.apache.commons.codec.binary.Hex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.HgNameWithHashInfo;
import org.zmlx.hg4idea.HgVcs;
import org.zmlx.hg4idea.util.HgVersion;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Reads information about the Hg repository from Hg service files located in the {@code .hg} folder.
* NB: works with {@link java.io.File}, i.e. reads from disk. Consider using caching.
* Throws a {@link RepoStateException} in the case of incorrect Hg file format.
*
* @author Nadya Zabrodina
*/
public class HgRepositoryReader {
private static Pattern HASH_NAME = Pattern.compile("\\s*([0-9a-fA-F]+)\\s+(.+)");
private static Pattern HASH_STATUS_NAME = Pattern.compile("\\s*([0-9a-fA-F]+)\\s+\\w\\s+(.+)");
//hash + name_or_revision_num; hash + status_character + name_or_revision_num
@NotNull private final File myHgDir; // .hg
@NotNull private File myBranchHeadsFile; // .hg/cache/branch* + part depends on version
@NotNull private final File myCacheDir; // .hg/cache (does not exist before first commit)
@NotNull private final File myCurrentBranch; // .hg/branch
@NotNull private final File myBookmarksFile; //.hg/bookmarks
@NotNull private final File myCurrentBookmark; //.hg/bookmarks.current
@NotNull private final File myTagsFile; //.hgtags - not in .hg directory!!!
@NotNull private final File myLocalTagsFile; // .hg/localtags
@NotNull private final File myDirStateFile; // .hg/dirstate
@NotNull private final File mySubrepoFile; // .hgsubstate
@NotNull private final VcsLogObjectsFactory myVcsObjectsFactory;
private final boolean myStatusInBranchFile;
@NotNull final HgVcs myVcs;
public HgRepositoryReader(@NotNull HgVcs vcs, @NotNull File hgDir) {
myHgDir = hgDir;
RepositoryUtil.assertFileExists(myHgDir, ".hg directory not found in " + myHgDir);
myVcs = vcs;
HgVersion version = myVcs.getVersion();
myStatusInBranchFile = version.hasBranch2();
myCacheDir = new File(myHgDir, "cache");
myBranchHeadsFile = identifyBranchHeadFile(version, myCacheDir);
myCurrentBranch = new File(myHgDir, "branch");
myBookmarksFile = new File(myHgDir, "bookmarks");
myCurrentBookmark = new File(myHgDir, "bookmarks.current");
myLocalTagsFile = new File(myHgDir, "localtags");
myTagsFile = new File(myHgDir.getParentFile(), ".hgtags");
mySubrepoFile = new File(myHgDir.getParentFile(), ".hgsubstate");
myDirStateFile = new File(myHgDir, "dirstate");
myVcsObjectsFactory = ServiceManager.getService(vcs.getProject(), VcsLogObjectsFactory.class);
}
/**
* Identify file with branches and heads information depends on hg version;
*/
@NotNull
private static File identifyBranchHeadFile(@NotNull HgVersion version, @NotNull File parentCacheFile) {
//before 2.5 only branchheads exist; branchheads-served after mercurial 2.5; branch2-served after 2.9;
// when project is recently cloned there are only base file
if (version.hasBranch2()) {
File file = new File(parentCacheFile, "branch2-served");
return file.exists() ? file : new File(parentCacheFile, "branch2-base");
}
if (version.hasBranchHeadsBaseServed()) {
File file = new File(parentCacheFile, "branchheads-served");
return file.exists() ? file : new File(parentCacheFile, "branchheads-base");
}
return new File(parentCacheFile, "branchheads");
}
/**
* Finds current revision value.
*
* @return The current revision hash, or <b>{@code null}</b> if current revision is unknown - it is the initial repository state.
*/
@Nullable
public String readCurrentRevision() {
if (!isDirStateInfoAvailable()) return null;
try {
return Hex.encodeHexString(readBytesFromFile(myDirStateFile, 20));
}
catch (IOException e) {
// dirState exists if not fresh, if we could not load dirState info repository must be corrupted
throw new RepoStateException("IOException while trying to read current repository state information.", e);
}
}
@NotNull
public byte[] readBytesFromFile(@NotNull File file, int len) throws IOException {
byte[] bytes;
final InputStream stream = new FileInputStream(file);
try {
bytes = FileUtil.loadBytes(stream, len);
}
finally {
stream.close();
}
return bytes;
}
/**
* Finds tip revision value.
*
* @return The tip revision hash, or <b>{@code null}</b> if tip revision is unknown - it is the initial repository state.
*/
@Nullable
public String readCurrentTipRevision() {
if (!isBranchInfoAvailable()) return null;
String[] branchesWithHeads = RepositoryUtil.tryLoadFile(myBranchHeadsFile).split("\n");
String head = branchesWithHeads[0];
Matcher matcher = HASH_NAME.matcher(head);
if (matcher.matches()) {
return (matcher.group(1));
}
return null;
}
private boolean isBranchInfoAvailable() {
myBranchHeadsFile = identifyBranchHeadFile(myVcs.getVersion(), myCacheDir);
return !isFresh() && myBranchHeadsFile.exists();
}
private boolean isDirStateInfoAvailable() {
return myDirStateFile.exists();
}
/**
* Return current branch
*/
@NotNull
public String readCurrentBranch() {
return branchExist() ? RepositoryUtil.tryLoadFile(myCurrentBranch) : HgRepository.DEFAULT_BRANCH;
}
@NotNull
public Map<String, Set<Hash>> readBranches() {
Map<String, Set<Hash>> branchesWithHashes = new HashMap<String, Set<Hash>>();
// Set<String> branchNames = new HashSet<String>();
if (isBranchInfoAvailable()) {
Pattern activeBranchPattern = myStatusInBranchFile ? HASH_STATUS_NAME : HASH_NAME;
String[] branchesWithHeads = RepositoryUtil.tryLoadFile(myBranchHeadsFile).split("\n");
// first one - is a head revision: head hash + head number;
for (int i = 1; i < branchesWithHeads.length; ++i) {
Matcher matcher = activeBranchPattern.matcher(branchesWithHeads[i]);
if (matcher.matches()) {
String name = matcher.group(2);
if (branchesWithHashes.containsKey(name)) {
branchesWithHashes.get(name).add(myVcsObjectsFactory.createHash(matcher.group(1)));
}
else {
Set<Hash> hashes = new HashSet<Hash>();
hashes.add(myVcsObjectsFactory.createHash(matcher.group(1)));
branchesWithHashes.put(name, hashes);
}
}
}
}
return branchesWithHashes;
}
public boolean isMergeInProgress() {
return new File(myHgDir, "merge").exists();
}
public boolean hasSubrepos() {
return mySubrepoFile.exists();
}
public boolean isRebaseInProgress() {
return new File(myHgDir, "rebasestate").exists();
}
@NotNull
public Repository.State readState() {
if (isRebaseInProgress()) {
return Repository.State.REBASING;
}
return isMergeInProgress() ? Repository.State.MERGING : Repository.State.NORMAL;
}
public boolean isFresh() {
return !myCacheDir.exists();
}
public boolean branchExist() {
return myCurrentBranch.exists();
}
@NotNull
public Collection<HgNameWithHashInfo> readBookmarks() {
return readReference(myBookmarksFile);
}
@NotNull
public Collection<HgNameWithHashInfo> readTags() {
return readReference(myTagsFile);
}
@NotNull
public Collection<HgNameWithHashInfo> readLocalTags() {
return readReference(myLocalTagsFile);
}
@NotNull
private Collection<HgNameWithHashInfo> readReference(@NotNull File fileWithReferences) {
// files like .hg/bookmarks which contains hash + name, f.e. 25e44c95b2612e3cdf29a704dabf82c77066cb67 A_BookMark
Set<HgNameWithHashInfo> refs = new HashSet<HgNameWithHashInfo>();
if (!fileWithReferences.exists()) {
return refs;
}
String[] namesWithHashes = RepositoryUtil.tryLoadFile(fileWithReferences).split("\n");
for (String str : namesWithHashes) {
Matcher matcher = HASH_NAME.matcher(str);
if (matcher.matches()) {
refs.add(new HgNameWithHashInfo(matcher.group(2), myVcsObjectsFactory.createHash(matcher.group(1))));
}
}
return refs;
}
@Nullable
public String readCurrentBookmark() {
return myCurrentBookmark.exists() ? RepositoryUtil.tryLoadFile(myCurrentBookmark) : null;
}
@NotNull
public Collection<HgNameWithHashInfo> readSubrepos() {
if (!hasSubrepos()) return Collections.emptySet();
return readReference(mySubrepoFile);
}
}