| /* |
| * 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 git4idea.changes; |
| |
| import com.intellij.openapi.diagnostic.Logger; |
| 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.FilePath; |
| 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.vfs.VirtualFile; |
| import com.intellij.util.ArrayUtil; |
| import git4idea.GitContentRevision; |
| import git4idea.GitRevisionNumber; |
| import git4idea.GitUtil; |
| import git4idea.commands.GitCommand; |
| import git4idea.commands.GitHandler; |
| import git4idea.commands.GitSimpleHandler; |
| import git4idea.history.browser.SHAHash; |
| import git4idea.history.wholeTree.AbstractHash; |
| import git4idea.util.StringScanner; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.io.File; |
| import java.util.*; |
| |
| /** |
| * Change related utilities |
| */ |
| public class GitChangeUtils { |
| /** |
| * the pattern for committed changelist assumed by {@link #parseChangeList(com.intellij.openapi.project.Project,com.intellij.openapi.vfs.VirtualFile, git4idea.util.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"; |
| |
| private static final Logger LOG = Logger.getInstance(GitChangeUtils.class); |
| |
| /** |
| * A private constructor for utility class |
| */ |
| private GitChangeUtils() { |
| } |
| |
| /** |
| * Parse changes from lines |
| * |
| * @param project the context project |
| * @param vcsRoot the git 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, |
| @Nullable GitRevisionNumber thisRevision, |
| GitRevisionNumber 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 + GitUtil.unescapePath(path); |
| path = FileUtil.toSystemDependentName(path); |
| result.add(path); |
| } |
| return result; |
| } |
| |
| /** |
| * Parse changes from lines |
| * |
| * @param project the context project |
| * @param vcsRoot the git 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, |
| @Nullable GitRevisionNumber thisRevision, |
| @Nullable GitRevisionNumber 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 ("CADUMRT".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 = GitContentRevision.createRevision(vcsRoot, path, thisRevision, project, false, false, true); |
| break; |
| case 'U': |
| status = FileStatus.MERGED_WITH_CONFLICTS; |
| case 'M': |
| if (status == null) { |
| status = FileStatus.MODIFIED; |
| } |
| before = GitContentRevision.createRevision(vcsRoot, path, parentRevision, project, false, true, true); |
| after = GitContentRevision.createRevision(vcsRoot, path, thisRevision, project, false, false, true); |
| break; |
| case 'D': |
| status = FileStatus.DELETED; |
| before = GitContentRevision.createRevision(vcsRoot, path, parentRevision, project, true, true, true); |
| after = null; |
| break; |
| case 'R': |
| status = FileStatus.MODIFIED; |
| before = GitContentRevision.createRevision(vcsRoot, tokens[1], parentRevision, project, true, true, true); |
| after = GitContentRevision.createRevision(vcsRoot, path, thisRevision, project, false, false, true); |
| break; |
| case 'T': |
| status = FileStatus.MODIFIED; |
| before = GitContentRevision.createRevision(vcsRoot, path, parentRevision, project, true, true, true); |
| after = GitContentRevision.createRevisionForTypeChange(project, vcsRoot, path, thisRevision, true); |
| 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 a reference: name of a branch or tag, or revision number expression. |
| */ |
| @NotNull |
| public static GitRevisionNumber resolveReference(@NotNull Project project, @NotNull VirtualFile vcsRoot, |
| @NotNull String reference) throws VcsException { |
| GitSimpleHandler handler = createRefResolveHandler(project, vcsRoot, reference); |
| String output = handler.run(); |
| StringTokenizer stk = new StringTokenizer(output, "\n\r \t", false); |
| if (!stk.hasMoreTokens()) { |
| try { |
| GitSimpleHandler dh = new GitSimpleHandler(project, vcsRoot, GitCommand.LOG); |
| dh.addParameters("-1", "HEAD"); |
| dh.setSilent(true); |
| String out = dh.run(); |
| LOG.info("Diagnostic output from 'git log -1 HEAD': [" + out + "]"); |
| dh = createRefResolveHandler(project, vcsRoot, reference); |
| out = dh.run(); |
| LOG.info("Diagnostic output from 'git rev-list -1 --timestamp HEAD': [" + out + "]"); |
| } |
| catch (VcsException e) { |
| LOG.info("Exception while trying to get some diagnostics info", e); |
| } |
| throw new VcsException(String.format("The string '%s' does not represent a revision number. Output: [%s]\n Root: %s", |
| reference, output, vcsRoot)); |
| } |
| Date timestamp = GitUtil.parseTimestampWithNFEReport(stk.nextToken(), handler, output); |
| return new GitRevisionNumber(stk.nextToken(), timestamp); |
| } |
| |
| @NotNull |
| private static GitSimpleHandler createRefResolveHandler(@NotNull Project project, @NotNull VirtualFile root, @NotNull String reference) { |
| GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.REV_LIST); |
| handler.addParameters("--timestamp", "--max-count=1", reference); |
| handler.endOptions(); |
| handler.setSilent(true); |
| return handler; |
| } |
| |
| /** |
| * 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 Git 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 git root |
| * @param revisionName the name of revision (might be tag) |
| * @param skipDiffsForMerge |
| * @param local |
| * @param revertable |
| * @return change list for the respective revision |
| * @throws VcsException in case of problem with running git |
| */ |
| public static GitCommittedChangeList getRevisionChanges(Project project, |
| VirtualFile root, |
| String revisionName, |
| boolean skipDiffsForMerge, |
| boolean local, boolean revertable) throws VcsException { |
| GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.SHOW); |
| h.setSilent(true); |
| h.addParameters("--name-status", "--first-parent", "--no-abbrev", "-M", "--pretty=format:" + COMMITTED_CHANGELIST_FORMAT, |
| "--encoding=UTF-8", |
| revisionName, "--"); |
| String output = h.run(); |
| StringScanner s = new StringScanner(output); |
| return parseChangeList(project, root, s, skipDiffsForMerge, h, local, revertable); |
| } |
| |
| @Nullable |
| public static String getCommitAbbreviation(final Project project, final VirtualFile root, final SHAHash hash) { |
| GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG); |
| 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, |
| List<VirtualFile> paths, final String... parameters) { |
| GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG); |
| h.setSilent(true); |
| h.addParameters(parameters); |
| h.addParameters("--max-count=1", "--pretty=%H", "--encoding=UTF-8", anyReference, "--"); |
| if (paths != null && ! paths.isEmpty()) { |
| h.addRelativeFiles(paths); |
| } |
| try { |
| final String output = h.run().trim(); |
| if (StringUtil.isEmptyOrSpaces(output)) return null; |
| return new SHAHash(output); |
| } |
| catch (VcsException e) { |
| return null; |
| } |
| } |
| |
| public static boolean isAnyLevelChild(final Project project, final VirtualFile root, final SHAHash parent, |
| final String anyReferenceChild) { |
| GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.MERGE_BASE); |
| h.setSilent(true); |
| h.addParameters("\"" + parent.getValue() + "\"","\"" + anyReferenceChild + "\"", "--"); |
| try { |
| final String output = h.run().trim(); |
| if (StringUtil.isEmptyOrSpaces(output)) return false; |
| return parent.getValue().equals(output.trim()); |
| } |
| catch (VcsException e) { |
| return false; |
| } |
| } |
| |
| @Nullable |
| public static List<AbstractHash> commitExistsByComment(final Project project, final VirtualFile root, final String anyReference) { |
| GitSimpleHandler h = new GitSimpleHandler(project, root, GitCommand.LOG); |
| h.setSilent(true); |
| String escaped = StringUtil.escapeQuotes(anyReference); |
| escaped = StringUtil.escapeSlashes(escaped); |
| final String grepParam = "--grep=" + escaped; |
| h.addParameters("--regexp-ignore-case", "--pretty=%h", "--all", "--encoding=UTF-8", grepParam, "--"); |
| try { |
| final String output = h.run().trim(); |
| if (StringUtil.isEmptyOrSpaces(output)) return null; |
| final String[] hashes = output.split("\n"); |
| final List<AbstractHash> result = new ArrayList<AbstractHash>(); |
| for (String hash : hashes) { |
| result.add(AbstractHash.create(hash)); |
| } |
| return result; |
| } |
| catch (VcsException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Parse changelist |
| * |
| * |
| * |
| * @param project the project |
| * @param root the git root |
| * @param s the scanner for log or show command output |
| * @param skipDiffsForMerge |
| * @param handler the handler that produced the output to parse. - for debugging purposes. |
| * @param local pass {@code true} to indicate that this revision should be an editable |
| * {@link com.intellij.openapi.vcs.changes.CurrentContentRevision}. |
| * Pass {@code false} for |
| * @param revertable |
| * @return the parsed changelist |
| * @throws VcsException if there is a problem with running git |
| */ |
| public static GitCommittedChangeList parseChangeList(Project project, |
| VirtualFile root, |
| StringScanner s, |
| boolean skipDiffsForMerge, |
| GitHandler handler, |
| boolean local, boolean revertable) throws VcsException { |
| ArrayList<Change> changes = new ArrayList<Change>(); |
| // parse commit information |
| final Date commitDate = GitUtil.parseTimestampWithNFEReport(s.line(), handler, s.getAllText()); |
| 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 = GitUtil.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 = commentSubject + "\n" + commentBody; |
| } |
| GitRevisionNumber thisRevision = new GitRevisionNumber(revisionNumber, commitDate); |
| |
| if (skipDiffsForMerge || (parents.length <= 1)) { |
| final GitRevisionNumber parentRevision = parents.length > 0 ? resolveReference(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, local ? null : 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 GitRevisionNumber parentRevision = resolveReference(project, root, parent); |
| GitSimpleHandler diffHandler = new GitSimpleHandler(project, root, GitCommand.DIFF); |
| diffHandler.setSilent(true); |
| diffHandler.addParameters("--name-status", "-M", parentRevision.getRev(), thisRevision.getRev()); |
| String diff = diffHandler.run(); |
| parseChanges(project, root, thisRevision, parentRevision, diff, changes, null); |
| |
| if (changes.size() > 0) { |
| break; |
| } |
| } |
| } |
| String changeListName = String.format("%s(%s)", commentSubject, revisionNumber); |
| return new GitCommittedChangeList(changeListName, fullComment, committerName, thisRevision, commitDate, changes, revertable); |
| } |
| |
| public static long longForSHAHash(String revisionNumber) { |
| return Long.parseLong(revisionNumber.substring(0, 15), 16) << 4 + Integer.parseInt(revisionNumber.substring(15, 16), 16); |
| } |
| |
| @NotNull |
| public static Collection<Change> getDiff(@NotNull Project project, @NotNull VirtualFile root, |
| @Nullable String oldRevision, @Nullable String newRevision, |
| @Nullable Collection<FilePath> dirtyPaths) throws VcsException { |
| LOG.assertTrue(oldRevision != null || newRevision != null, "Both old and new revisions can't be null"); |
| String range; |
| GitRevisionNumber newRev; |
| GitRevisionNumber oldRev; |
| if (newRevision == null) { // current revision at the right |
| range = oldRevision + ".."; |
| oldRev = resolveReference(project, root, oldRevision); |
| newRev = null; |
| } |
| else if (oldRevision == null) { // current revision at the left |
| range = ".." + newRevision; |
| oldRev = null; |
| newRev = resolveReference(project, root, newRevision); |
| } |
| else { |
| range = oldRevision + ".." + newRevision; |
| oldRev = resolveReference(project, root, oldRevision); |
| newRev = resolveReference(project, root, newRevision); |
| } |
| String output = getDiffOutput(project, root, range, dirtyPaths); |
| |
| Collection<Change> changes = new ArrayList<Change>(); |
| parseChanges(project, root, newRev, oldRev, output, changes, Collections.<String>emptySet()); |
| return changes; |
| } |
| |
| /** |
| * Calls {@code git diff} on the given range. |
| * @param project |
| * @param root |
| * @param diffRange range or just revision (will be compared with current working tree). |
| * @param dirtyPaths limit the command by paths if needed or pass null. |
| * @return output of the 'git diff' command. |
| * @throws VcsException |
| */ |
| @NotNull |
| public static String getDiffOutput(@NotNull Project project, @NotNull VirtualFile root, |
| @NotNull String diffRange, @Nullable Collection<FilePath> dirtyPaths) throws VcsException { |
| GitSimpleHandler handler = getDiffHandler(project, root, diffRange, dirtyPaths); |
| if (handler.isLargeCommandLine()) { |
| // if there are too much files, just get all changes for the project |
| handler = getDiffHandler(project, root, diffRange, null); |
| } |
| return handler.run(); |
| } |
| |
| @NotNull |
| private static GitSimpleHandler getDiffHandler(@NotNull Project project, @NotNull VirtualFile root, |
| @NotNull String diffRange, @Nullable Collection<FilePath> dirtyPaths) { |
| GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.DIFF); |
| handler.addParameters("--name-status", "--diff-filter=ADCMRUXT", "-M", diffRange); |
| handler.setSilent(true); |
| handler.setStdoutSuppressed(true); |
| handler.endOptions(); |
| if (dirtyPaths != null) { |
| handler.addRelativePaths(dirtyPaths); |
| } |
| return handler; |
| } |
| |
| } |