tikhomirov@50: /* tikhomirov@535: * Copyright (c) 2011-2013 TMate Software Ltd tikhomirov@74: * tikhomirov@74: * This program is free software; you can redistribute it and/or modify tikhomirov@74: * it under the terms of the GNU General Public License as published by tikhomirov@74: * the Free Software Foundation; version 2 of the License. tikhomirov@74: * tikhomirov@74: * This program is distributed in the hope that it will be useful, tikhomirov@74: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@74: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@74: * GNU General Public License for more details. tikhomirov@74: * tikhomirov@74: * For information on how to redistribute this software under tikhomirov@74: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@50: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@50: tikhomirov@610: import static org.tmatesoft.hg.repo.HgRepositoryFiles.HgLocalTags; tikhomirov@610: import static org.tmatesoft.hg.repo.HgRepositoryFiles.HgTags; tikhomirov@610: import static org.tmatesoft.hg.util.LogFacility.Severity.*; tikhomirov@535: import static org.tmatesoft.hg.util.LogFacility.Severity.Error; tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; tikhomirov@456: tikhomirov@104: import java.io.BufferedReader; tikhomirov@104: import java.io.File; tikhomirov@104: import java.io.FileReader; tikhomirov@104: import java.io.IOException; tikhomirov@234: import java.io.Reader; tikhomirov@610: import java.io.StringReader; tikhomirov@104: import java.util.ArrayList; tikhomirov@234: import java.util.Collections; tikhomirov@104: import java.util.HashMap; tikhomirov@104: import java.util.LinkedList; tikhomirov@50: import java.util.List; tikhomirov@104: import java.util.Map; tikhomirov@104: import java.util.TreeMap; tikhomirov@50: tikhomirov@535: import org.tmatesoft.hg.core.HgBadNodeidFormatException; tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@610: import org.tmatesoft.hg.internal.ByteArrayChannel; tikhomirov@610: import org.tmatesoft.hg.internal.ChangelogMonitor; tikhomirov@610: import org.tmatesoft.hg.internal.FileChangeMonitor; tikhomirov@610: import org.tmatesoft.hg.internal.Internals; tikhomirov@610: import org.tmatesoft.hg.util.CancelledException; tikhomirov@74: tikhomirov@50: /** tikhomirov@104: * @see http://mercurial.selenic.com/wiki/TagDesign tikhomirov@74: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@50: */ tikhomirov@50: public class HgTags { tikhomirov@104: // global tags come from ".hgtags" tikhomirov@104: // local come from ".hg/localtags" tikhomirov@104: tikhomirov@610: private final Internals repo; tikhomirov@234: tikhomirov@104: private final Map> globalToName; tikhomirov@104: private final Map> localToName; tikhomirov@104: private final Map> globalFromName; tikhomirov@104: private final Map> localFromName; tikhomirov@104: tikhomirov@610: private FileChangeMonitor globalTagsFileMonitor, localTagsFileMonitor; tikhomirov@610: private ChangelogMonitor repoChangeMonitor; tikhomirov@610: tikhomirov@234: private Map tags; tikhomirov@104: tikhomirov@610: /*package-local*/ HgTags(Internals internalRepo) { tikhomirov@610: repo = internalRepo; tikhomirov@104: globalToName = new HashMap>(); tikhomirov@104: localToName = new HashMap>(); tikhomirov@104: globalFromName = new TreeMap>(); tikhomirov@104: localFromName = new TreeMap>(); tikhomirov@104: } tikhomirov@104: tikhomirov@610: /*package-local*/ void read() throws HgInvalidControlFileException { tikhomirov@610: readTagsFromHistory(); tikhomirov@610: readGlobal(); tikhomirov@610: readLocal(); tikhomirov@104: } tikhomirov@104: tikhomirov@610: private void readTagsFromHistory() throws HgInvalidControlFileException { tikhomirov@610: HgDataFile hgTags = repo.getRepo().getFileNode(HgTags.getPath()); tikhomirov@610: if (hgTags.exists()) { tikhomirov@610: for (int i = 0; i <= hgTags.getLastRevision(); i++) { // TODO post-1.0 in fact, would be handy to have walk(start,end) tikhomirov@610: // method for data files as well, though it looks odd. tikhomirov@610: try { tikhomirov@610: ByteArrayChannel sink = new ByteArrayChannel(); tikhomirov@610: hgTags.content(i, sink); tikhomirov@610: final String content = new String(sink.toArray(), "UTF8"); tikhomirov@610: readGlobal(new StringReader(content)); tikhomirov@610: } catch (CancelledException ex) { tikhomirov@610: // IGNORE, can't happen, we did not configure cancellation tikhomirov@610: repo.getLog().dump(getClass(), Debug, ex, null); tikhomirov@610: } catch (IOException ex) { tikhomirov@610: // UnsupportedEncodingException can't happen (UTF8) tikhomirov@610: // only from readGlobal. Need to reconsider exceptions thrown from there: tikhomirov@610: // BufferedReader wraps String and unlikely to throw IOException, perhaps, log is enough? tikhomirov@610: repo.getLog().dump(getClass(), Error, ex, null); tikhomirov@610: // XXX need to decide what to do this. failure to read single revision shall not break complete cycle tikhomirov@610: } tikhomirov@610: } tikhomirov@104: } tikhomirov@610: if (repoChangeMonitor == null) { tikhomirov@610: repoChangeMonitor = new ChangelogMonitor(repo.getRepo()); tikhomirov@610: } tikhomirov@610: repoChangeMonitor.touch(); tikhomirov@610: } tikhomirov@610: tikhomirov@610: private void readLocal() throws HgInvalidControlFileException { tikhomirov@610: File localTags = repo.getRepositoryFile(HgLocalTags); tikhomirov@610: if (localTags.canRead() && localTags.isFile()) { tikhomirov@610: read(localTags, localToName, localFromName); tikhomirov@610: } tikhomirov@610: if (localTagsFileMonitor == null) { tikhomirov@610: localTagsFileMonitor = new FileChangeMonitor(localTags); tikhomirov@610: } tikhomirov@610: localTagsFileMonitor.touch(this); tikhomirov@610: } tikhomirov@610: tikhomirov@610: private void readGlobal() throws HgInvalidControlFileException { tikhomirov@610: File globalTags = repo.getRepositoryFile(HgTags); // XXX replace with HgDataFile.workingCopy tikhomirov@610: if (globalTags.canRead() && globalTags.isFile()) { tikhomirov@610: read(globalTags, globalToName, globalFromName); tikhomirov@610: } tikhomirov@610: if (globalTagsFileMonitor == null) { tikhomirov@610: globalTagsFileMonitor = new FileChangeMonitor(globalTags); tikhomirov@610: } tikhomirov@610: globalTagsFileMonitor.touch(this); tikhomirov@104: } tikhomirov@234: tikhomirov@610: private void readGlobal(Reader globalTags) throws IOException { tikhomirov@234: BufferedReader r = null; tikhomirov@234: try { tikhomirov@234: r = new BufferedReader(globalTags); tikhomirov@234: read(r, globalToName, globalFromName); tikhomirov@234: } finally { tikhomirov@234: if (r != null) { tikhomirov@234: r.close(); tikhomirov@234: } tikhomirov@234: } tikhomirov@234: } tikhomirov@104: tikhomirov@610: private void read(File f, Map> nid2name, Map> name2nid) throws HgInvalidControlFileException { tikhomirov@104: if (!f.canRead()) { tikhomirov@104: return; tikhomirov@104: } tikhomirov@104: BufferedReader r = null; tikhomirov@104: try { tikhomirov@104: r = new BufferedReader(new FileReader(f)); tikhomirov@104: read(r, nid2name, name2nid); tikhomirov@610: } catch (IOException ex) { tikhomirov@610: repo.getLog().dump(getClass(), Error, ex, null); tikhomirov@610: throw new HgInvalidControlFileException("Failed to read tags", ex, f); tikhomirov@104: } finally { tikhomirov@104: if (r != null) { tikhomirov@610: try { tikhomirov@610: r.close(); tikhomirov@610: } catch (IOException ex) { tikhomirov@610: // since it's read operation, do not treat close failure as error, but let user know, anyway tikhomirov@610: repo.getLog().dump(getClass(), Warn, ex, null); tikhomirov@610: } tikhomirov@104: } tikhomirov@104: } tikhomirov@104: } tikhomirov@104: tikhomirov@104: private void read(BufferedReader reader, Map> nid2name, Map> name2nid) throws IOException { tikhomirov@104: String line; tikhomirov@104: while ((line = reader.readLine()) != null) { tikhomirov@104: line = line.trim(); tikhomirov@104: if (line.length() == 0) { tikhomirov@104: continue; tikhomirov@104: } tikhomirov@535: final int spacePos = line.indexOf(' '); tikhomirov@535: if (line.length() < 40+2 /*nodeid, space and at least single-char tagname*/ || spacePos != 40) { tikhomirov@610: repo.getLog().dump(getClass(), Warn, "Bad tags line: %s", line); tikhomirov@104: continue; tikhomirov@104: } tikhomirov@535: try { tikhomirov@104: assert spacePos == 40; tikhomirov@104: final byte[] nodeidBytes = line.substring(0, spacePos).getBytes(); tikhomirov@104: Nodeid nid = Nodeid.fromAscii(nodeidBytes, 0, nodeidBytes.length); tikhomirov@104: String tagName = line.substring(spacePos+1); tikhomirov@104: List nids = name2nid.get(tagName); tikhomirov@104: if (nids == null) { tikhomirov@104: nids = new LinkedList(); tikhomirov@234: nids.add(nid); tikhomirov@104: // tagName is substring of full line, thus need a copy to let the line be GC'ed tikhomirov@104: // new String(tagName.toCharArray()) is more expressive, but results in 1 extra arraycopy tikhomirov@104: tagName = new String(tagName); tikhomirov@104: name2nid.put(tagName, nids); tikhomirov@234: } else if (!nid.equals(nids.get(0))) { tikhomirov@234: // Alternatively, !nids.contains(nid) might have come to mind. tikhomirov@234: // However, I guess that 'tag history' means we need to record each change of revision tikhomirov@234: // associated with the tag, i.e. imagine project evolution: tikhomirov@234: // tag1=r1, tag1=r2, tag1=r1. If we choose !contains, list top of tag1 would point to r2 tikhomirov@234: // while we need it to point to r1. tikhomirov@234: // In fact, there are still possible odd patterns in name2nid list, e.g. tikhomirov@234: // when tag was removed and added back(initially rev1 tag1, on removal *added* nullrev tag1), tikhomirov@234: // then added back (rev2 tag1). tikhomirov@234: // name2nid would list (rev2 nullrev rev1) as many times, as there were revisions of the .hgtags file tikhomirov@234: // See cpython "v2.4.3c1" revision for example. tikhomirov@234: // It doesn't seem to hurt (unless there are clients that care about tag history and depend on tikhomirov@234: // unique revisions there), XXX but better to be fixed (not sure how, though) tikhomirov@234: ((LinkedList) nids).addFirst(nid); tikhomirov@234: // XXX repo.getNodeidCache().nodeid(nid); tikhomirov@104: } tikhomirov@104: List revTags = nid2name.get(nid); tikhomirov@104: if (revTags == null) { tikhomirov@104: revTags = new LinkedList(); tikhomirov@234: revTags.add(tagName); tikhomirov@104: nid2name.put(nid, revTags); tikhomirov@234: } else if (!revTags.contains(tagName)) { tikhomirov@234: // !contains because we don't care about order of the tags per revision tikhomirov@234: revTags.add(tagName); tikhomirov@104: } tikhomirov@535: } catch (HgBadNodeidFormatException ex) { tikhomirov@610: repo.getLog().dump(getClass(), Error, "Bad revision '%s' in line '%s':%s", line.substring(0, spacePos), line, ex.getMessage()); tikhomirov@104: } tikhomirov@104: } tikhomirov@104: } tikhomirov@50: tikhomirov@50: public List tags(Nodeid nid) { tikhomirov@104: ArrayList rv = new ArrayList(5); tikhomirov@104: List l; tikhomirov@104: if ((l = localToName.get(nid)) != null) { tikhomirov@104: rv.addAll(l); tikhomirov@104: } tikhomirov@104: if ((l = globalToName.get(nid)) != null) { tikhomirov@104: rv.addAll(l); tikhomirov@104: } tikhomirov@104: return rv; tikhomirov@50: } tikhomirov@50: tikhomirov@50: public boolean isTagged(Nodeid nid) { tikhomirov@104: return localToName.containsKey(nid) || globalToName.containsKey(nid); tikhomirov@104: } tikhomirov@104: tikhomirov@104: public List tagged(String tagName) { tikhomirov@104: ArrayList rv = new ArrayList(5); tikhomirov@104: List l; tikhomirov@104: if ((l = localFromName.get(tagName)) != null) { tikhomirov@104: rv.addAll(l); tikhomirov@104: } tikhomirov@104: if ((l = globalFromName.get(tagName)) != null) { tikhomirov@104: rv.addAll(l); tikhomirov@104: } tikhomirov@104: return rv; tikhomirov@50: } tikhomirov@344: tikhomirov@344: /** tikhomirov@344: * All tag entries from the repository, for both active and removed tags tikhomirov@344: */ tikhomirov@344: public Map getAllTags() { tikhomirov@234: if (tags == null) { tikhomirov@234: tags = new TreeMap(); tikhomirov@234: for (String t : globalFromName.keySet()) { tikhomirov@234: tags.put(t, new TagInfo(t)); tikhomirov@234: } tikhomirov@234: for (String t : localFromName.keySet()) { tikhomirov@234: tags.put(t, new TagInfo(t)); tikhomirov@234: } tikhomirov@234: tags = Collections.unmodifiableMap(tags); tikhomirov@234: } tikhomirov@234: return tags; tikhomirov@234: } tikhomirov@344: tikhomirov@344: /** tikhomirov@344: * Tags that are in use in the repository, unlike {@link #getAllTags()} doesn't list removed tags. tikhomirov@344: */ tikhomirov@344: public Map getActiveTags() { tikhomirov@344: TreeMap rv = new TreeMap(); tikhomirov@344: for (Map.Entry e : getAllTags().entrySet()) { tikhomirov@344: if (!e.getValue().isRemoved()) { tikhomirov@344: rv.put(e.getKey(), e.getValue()); tikhomirov@344: } tikhomirov@344: } tikhomirov@344: return rv; tikhomirov@344: } tikhomirov@234: tikhomirov@610: // can be called only after instance has been initialized (#read() invoked) tikhomirov@610: /*package-local*/void reloadIfChanged() throws HgInvalidControlFileException { tikhomirov@610: assert repoChangeMonitor != null; tikhomirov@610: assert localTagsFileMonitor != null; tikhomirov@610: assert globalTagsFileMonitor != null; tikhomirov@610: if (repoChangeMonitor.isChanged() || globalTagsFileMonitor.changed(this)) { tikhomirov@610: globalFromName.clear(); tikhomirov@610: globalToName.clear(); tikhomirov@610: readTagsFromHistory(); tikhomirov@610: readGlobal(); tikhomirov@610: tags = null; tikhomirov@610: } tikhomirov@610: if (localTagsFileMonitor.changed(this)) { tikhomirov@610: readLocal(); tikhomirov@610: tags = null; tikhomirov@610: } tikhomirov@610: } tikhomirov@234: tikhomirov@234: public final class TagInfo { tikhomirov@234: private final String name; tikhomirov@234: private String branch; tikhomirov@234: tikhomirov@234: TagInfo(String tagName) { tikhomirov@344: name = tagName; tikhomirov@234: } tikhomirov@234: public String name() { tikhomirov@234: return name; tikhomirov@234: } tikhomirov@234: tikhomirov@234: public boolean isLocal() { tikhomirov@234: return localFromName.containsKey(name); tikhomirov@234: } tikhomirov@234: tikhomirov@354: public String branch() throws HgInvalidControlFileException { tikhomirov@234: if (branch == null) { tikhomirov@610: int x = repo.getRepo().getChangelog().getRevisionIndex(revision()); tikhomirov@610: branch = repo.getRepo().getChangelog().range(x, x).get(0).branch(); tikhomirov@234: } tikhomirov@234: return branch; tikhomirov@234: } tikhomirov@234: public Nodeid revision() { tikhomirov@234: if (isLocal()) { tikhomirov@234: return localFromName.get(name).get(0); tikhomirov@234: } tikhomirov@234: return globalFromName.get(name).get(0); tikhomirov@234: } tikhomirov@344: tikhomirov@344: /** tikhomirov@344: * @return true if this tag entry describes tag removal tikhomirov@344: */ tikhomirov@344: public boolean isRemoved() { tikhomirov@344: return revision().isNull(); tikhomirov@344: } tikhomirov@234: } tikhomirov@50: }