/* * Copyright 2000-2009 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.community.intellij.plugins.communitycase.changes; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.FileStatus; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ContentRevision; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeListImpl; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; import org.community.intellij.plugins.communitycase.Util; import org.community.intellij.plugins.communitycase.commands.Command; import org.community.intellij.plugins.communitycase.commands.SimpleHandler; import org.community.intellij.plugins.communitycase.commands.StringScanner; import org.community.intellij.plugins.communitycase.history.HistoryUtils; import org.community.intellij.plugins.communitycase.history.browser.ShaHash; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.*; /** * Change related utilities */ public class ChangeUtils { /** * the pattern for committed changelist assumed by {@link #parseChangeList(com.intellij.openapi.project.Project,com.intellij.openapi.vfs.VirtualFile,org.community.intellij.plugins.communitycase.commands.StringScanner,boolean)} */ public static final String COMMITTED_CHANGELIST_FORMAT = "%ct%n%H%n%P%n%an%x20%x3C%ae%x3E%n%cn%x20%x3C%ce%x3E%n%s%n%x03%n%b%n%x03"; /** * A private constructor for utility class */ private ChangeUtils() { } /** * Parse changes from lines * * @param project the context project * @param root the root * @return a set of unmerged files * @throws VcsException if the input format does not matches expected format */ public static List<VirtualFile> unmergedFiles(Project project, VirtualFile root) throws VcsException { HashSet<VirtualFile> unmerged = new HashSet<VirtualFile>(); String rootPath = root.getPath(); SimpleHandler h = new SimpleHandler(project, root, Command.LS_FILES); h.setRemote(true); h.setSilent(true); h.addParameters("--unmerged"); LocalFileSystem lfs = LocalFileSystem.getInstance(); for (StringScanner s = new StringScanner(h.run()); s.hasMoreData();) { if (s.isEol()) { s.nextLine(); continue; } s.boundedToken('\t'); final String relative = s.line(); String path = rootPath + "/" + Util.unescapePath(relative); VirtualFile file = lfs.refreshAndFindFileByPath(path); assert file != null : "The unmerged file is not found " + path; file.refresh(false, false); unmerged.add(file); } if (unmerged.size() == 0) { return Collections.emptyList(); } else { ArrayList<VirtualFile> rc = new ArrayList<VirtualFile>(unmerged.size()); rc.addAll(unmerged); Collections.sort(rc, Util.VIRTUAL_FILE_COMPARATOR); return rc; } } /** * Parse changes from lines * * @param project the context project * @param vcsRoot the root * @param thisRevision the current revision * @param parentRevision the parent revision for this change list * @param s the lines to parse * @param changes a list of changes to update * @param ignoreNames a set of names ignored during collection of the changes * @throws VcsException if the input format does not matches expected format */ public static void parseChanges(Project project, VirtualFile vcsRoot, VcsRevisionNumber thisRevision, VcsRevisionNumber parentRevision, String s, Collection<Change> changes, final Set<String> ignoreNames) throws VcsException { StringScanner sc = new StringScanner(s); parseChanges(project, vcsRoot, thisRevision, parentRevision, sc, changes, ignoreNames); if (sc.hasMoreData()) { throw new IllegalStateException("Unknown file status: " + sc.line()); } } public static Collection<String> parseDiffForPaths(final String rootPath, final StringScanner s) throws VcsException { final Collection<String> result = new ArrayList<String>(); while (s.hasMoreData()) { if (s.isEol()) { s.nextLine(); continue; } if ("CADUMR".indexOf(s.peek()) == -1) { // exit if there is no next character break; } assert 'M' != s.peek() : "Moves are not yet handled"; String[] tokens = s.line().split("\t"); String path = tokens[tokens.length - 1]; path = rootPath + File.separator + Util.unescapePath(path); path = FileUtil.toSystemDependentName(path); result.add(path); } return result; } /** * Parse changes from lines * * @param project the context project * @param vcsRoot the root * @param thisRevision the current revision * @param parentRevision the parent revision for this change list * @param s the lines to parse * @param changes a list of changes to update * @param ignoreNames a set of names ignored during collection of the changes * @throws VcsException if the input format does not matches expected format */ public static void parseChanges(Project project, VirtualFile vcsRoot, VcsRevisionNumber thisRevision, VcsRevisionNumber parentRevision, StringScanner s, Collection<Change> changes, final Set<String> ignoreNames) throws VcsException { while (s.hasMoreData()) { FileStatus status = null; if (s.isEol()) { s.nextLine(); continue; } if ("CADUMR".indexOf(s.peek()) == -1) { // exit if there is no next character return; } String[] tokens = s.line().split("\t"); final ContentRevision before; final ContentRevision after; final String path = tokens[tokens.length - 1]; switch (tokens[0].charAt(0)) { case 'C': case 'A': before = null; status = FileStatus.ADDED; after = org.community.intellij.plugins.communitycase.ContentRevision.createRevision(vcsRoot, path, thisRevision, project, false, false); break; case 'U': status = FileStatus.MERGED_WITH_CONFLICTS; case 'M': if (status == null) { status = FileStatus.MODIFIED; } before = org.community.intellij.plugins.communitycase.ContentRevision.createRevision(vcsRoot, path, parentRevision, project, false, true); after = org.community.intellij.plugins.communitycase.ContentRevision.createRevision(vcsRoot, path, thisRevision, project, false, false); break; case 'D': status = FileStatus.DELETED; before = org.community.intellij.plugins.communitycase.ContentRevision.createRevision(vcsRoot, path, parentRevision, project, true, true); after = null; break; case 'R': status = FileStatus.MODIFIED; before = org.community.intellij.plugins.communitycase.ContentRevision.createRevision(vcsRoot, tokens[1], parentRevision, project, true, true); after = org.community.intellij.plugins.communitycase.ContentRevision.createRevision(vcsRoot, path, thisRevision, project, false, false); break; default: throw new VcsException("Unknown file status: " + Arrays.asList(tokens)); } if (ignoreNames == null || !ignoreNames.contains(path)) { changes.add(new Change(before, after, status)); } } } /** * Load actual revision number with timestamp basing on revision number expression * * @param project a project * @param vcsRoot a repository root * @param revisionNumber a revision number expression * @return a resolved revision * @throws VcsException if there is a problem with running */ @SuppressWarnings({"SameParameterValue"}) public static VcsRevisionNumber loadRevision(final Project project, final VirtualFile vcsRoot, @NonNls final String revisionNumber) throws VcsException { SimpleHandler handler = new SimpleHandler(project, vcsRoot, Command.REV_LIST); handler.addParameters("--timestamp", "--max-count=1", revisionNumber); handler.endOptions(); handler.setRemote(true); //handler.setSilent(true); String output = handler.run(); StringTokenizer stk = new StringTokenizer(output, "\n\r \t", false); if (!stk.hasMoreTokens()) { throw new VcsException("The string '" + revisionNumber + "' does not represents a revision number."); } Date timestamp = Util.parseTimestamp(stk.nextToken()); return HistoryUtils.createUnvalidatedRevisionNumber(stk.nextToken()); } /** * Check if the exception means that HEAD is missing for the current repository. * * @param e the exception to examine * @return true if the head is missing */ public static boolean isHeadMissing(final VcsException e) { @NonNls final String errorText = "fatal: bad revision 'HEAD'\n"; return e.getMessage().equals(errorText); } /** * Get list of changes. Because native non-linear revision tree structure is not * supported by the current IDEA interfaces some simplifications are made in the case * of the merge, so changes are reported as difference with the first revision * listed on the the merge that has at least some changes. * * @param project the project file * @param root the root * @param revisionName the name of revision (might be tag) * @param skipDiffsForMerge * @return change list for the respective revision * @throws VcsException in case of problem with running */ public static CommittedChangeList getRevisionChanges(Project project, VirtualFile root, String revisionName, boolean skipDiffsForMerge) throws VcsException { SimpleHandler h = new SimpleHandler(project, root, Command.SHOW); h.setRemote(true); h.setSilent(true); h.addParameters("--name-status", "--no-abbrev", "-M", "--pretty=format:" + COMMITTED_CHANGELIST_FORMAT, "--encoding=UTF-8", revisionName, "--"); String output = h.run(); StringScanner s = new StringScanner(output); try { return parseChangeList(project, root, s, skipDiffsForMerge); } catch (RuntimeException e) { throw e; } catch (VcsException e) { throw e; } catch (Exception e) { throw new VcsException(e); } } @Nullable public static String getCommitAbbreviation(final Project project, final VirtualFile root, final ShaHash hash) { SimpleHandler h = new SimpleHandler(project, root, Command.LOG); h.setRemote(true); h.setSilent(true); h.addParameters("--max-count=1", "--pretty=%h", "--encoding=UTF-8", "\"" + hash.getValue() + "\"", "--"); try { final String output = h.run().trim(); if (StringUtil.isEmptyOrSpaces(output)) return null; return output.trim(); } catch (VcsException e) { return null; } } @Nullable public static ShaHash commitExists(final Project project, final VirtualFile root, final String anyReference) { SimpleHandler h = new SimpleHandler(project, root, Command.LOG); h.setRemote(true); h.setSilent(true); h.addParameters("--max-count=1", "--pretty=%H", "--encoding=UTF-8", "\"" + anyReference + "\"", "--"); try { final String output = h.run().trim(); if (StringUtil.isEmptyOrSpaces(output)) return null; return new ShaHash(output); } catch (VcsException e) { return null; } } @Nullable public static ShaHash commitExistsByComment(final Project project, final VirtualFile root, final String anyReference) { SimpleHandler h = new SimpleHandler(project, root, Command.LOG); h.setRemote(true); h.setSilent(true); final String grepParam = "--grep=" + StringUtil.escapeQuotes(anyReference); h.addParameters("--max-count=1", "--pretty=%H", "--all", "--encoding=UTF-8", grepParam, "--"); try { final String output = h.run().trim(); if (StringUtil.isEmptyOrSpaces(output)) return null; return new ShaHash(output); } catch (VcsException e) { return null; } } /** * Parse changelist * * @param project the project * @param root the root * @param s the scanner for log or show command output * @param skipDiffsForMerge * @return the parsed changelist * @throws VcsException if there is a problem with running */ public static CommittedChangeList parseChangeList(Project project, VirtualFile root, StringScanner s, boolean skipDiffsForMerge) throws VcsException { ArrayList<Change> changes = new ArrayList<Change>(); // parse commit information final Date commitDate = Util.parseTimestamp(s.line()); final String revisionNumber = s.line(); final String parentsLine = s.line(); final String[] parents = parentsLine.length() == 0 ? ArrayUtil.EMPTY_STRING_ARRAY : parentsLine.split(" "); String authorName = s.line(); String committerName = s.line(); committerName = Util.adjustAuthorName(authorName, committerName); String commentSubject = s.boundedToken('\u0003', true); s.nextLine(); String commentBody = s.boundedToken('\u0003', true); // construct full comment String fullComment; if (commentSubject.length() == 0) { fullComment = commentBody; } else if (commentBody.length() == 0) { fullComment = commentSubject; } else { fullComment = commentBody + "\n\n" + commentSubject; } VcsRevisionNumber thisRevision=HistoryUtils.createUnvalidatedRevisionNumber(revisionNumber); long number = longForShaHash(revisionNumber); if (skipDiffsForMerge || (parents.length <= 1)) { final VcsRevisionNumber parentRevision = parents.length > 0 ? loadRevision(project, root, parents[0]) : null; // This is the first or normal commit with the single parent. // Just parse changes in this commit as returned by the show command. parseChanges(project, root, thisRevision, parentRevision, s, changes, null); } else { // This is the merge commit. It has multiple parent commits. // Find the first commit with changes and report it as a change list. // If no changes are found (why to merge then?). Empty changelist is reported. for (String parent : parents) { final VcsRevisionNumber parentRevision = loadRevision(project, root, parent); if (parentRevision == null) { // the repository was cloned with --depth parameter continue; } SimpleHandler diffHandler = new SimpleHandler(project, root, Command.DIFF); diffHandler.setRemote(true); diffHandler.setSilent(true); diffHandler.addParameters("--name-status", "-M", parentRevision.asString(), thisRevision.asString()); String diff = diffHandler.run(); parseChanges(project, root, thisRevision, parentRevision, diff, changes, null); if (changes.size() > 0) { break; } } } return new CommittedChangeListImpl(commentSubject + "(" + revisionNumber + ")", fullComment, committerName, number, commitDate, changes); } public static long longForShaHash(String revisionNumber) { return Long.parseLong(revisionNumber.substring(0, 15), 16) << 4 + Integer.parseInt(revisionNumber.substring(15, 16), 16); } }