summaryrefslogtreecommitdiff
path: root/plugins/svn4idea/src/org/jetbrains/idea/svn/diff/CmdDiffClient.java
blob: be084b0986d01bf3a7cc23ebe8c2f5276c7f7128 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/*
 * 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.jetbrains.idea.svn.diff;

import com.intellij.openapi.util.io.FileUtil;
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.vcs.changes.CurrentContentRevision;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnStatusConvertor;
import org.jetbrains.idea.svn.SvnUtil;
import org.jetbrains.idea.svn.WorkingCopyFormat;
import org.jetbrains.idea.svn.api.BaseSvnClient;
import org.jetbrains.idea.svn.api.NodeKind;
import org.jetbrains.idea.svn.commandLine.CommandExecutor;
import org.jetbrains.idea.svn.commandLine.CommandUtil;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.commandLine.SvnCommandName;
import org.jetbrains.idea.svn.history.SvnRepositoryContentRevision;
import org.jetbrains.idea.svn.status.SvnStatusHandler;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc2.SvnTarget;

import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.*;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Konstantin Kolosovsky.
 */
public class CmdDiffClient extends BaseSvnClient implements DiffClient {

  @NotNull
  @Override
  public List<Change> compare(@NotNull SvnTarget target1, @NotNull SvnTarget target2) throws VcsException {
    assertUrl(target1);
    if (target2.isFile()) {
      // Such combination (file and url) with "--summarize" option is supported only in svn 1.8.
      // For svn 1.7 "--summarize" is only supported when both targets are repository urls.
      assertDirectory(target2);

      WorkingCopyFormat format = WorkingCopyFormat.from(myFactory.createVersionClient().getVersion());
      if (format.less(WorkingCopyFormat.ONE_DOT_EIGHT)) {
        throw new SvnBindException("Could not compare local file and remote url with executable for svn " + format);
      }
    }

    List<String> parameters = new ArrayList<String>();
    CommandUtil.put(parameters, target1);
    CommandUtil.put(parameters, target2);
    parameters.add("--xml");
    parameters.add("--summarize");

    CommandExecutor executor = execute(myVcs, target1, SvnCommandName.diff, parameters, null);
    return parseOutput(target1, target2, executor);
  }

  @Override
  public void unifiedDiff(@NotNull SvnTarget target1, @NotNull SvnTarget target2, @NotNull OutputStream output) throws VcsException {
    assertUrl(target1);
    assertUrl(target2);

    List<String> parameters = ContainerUtil.newArrayList();
    CommandUtil.put(parameters, target1);
    CommandUtil.put(parameters, target2);

    CommandExecutor executor = execute(myVcs, target1, SvnCommandName.diff, parameters, null);

    try {
      executor.getBinaryOutput().writeTo(output);
    }
    catch (IOException e) {
      throw new SvnBindException(e);
    }
  }

  @NotNull
  private List<Change> parseOutput(@NotNull SvnTarget target1, @NotNull SvnTarget target2, @NotNull CommandExecutor executor)
    throws SvnBindException {
    try {
      DiffInfo diffInfo = CommandUtil.parse(executor.getOutput(), DiffInfo.class);
      List<Change> result = ContainerUtil.newArrayList();

      if (diffInfo != null) {
        for (DiffPath path : diffInfo.diffPaths) {
          result.add(createChange(target1, target2, path));
        }
      }

      return result;
    }
    catch (JAXBException e) {
      throw new SvnBindException(e);
    }
  }

  @NotNull
  private ContentRevision createRevision(@NotNull FilePath path,
                                         @NotNull FilePath localPath,
                                         @NotNull SVNRevision revision,
                                         @NotNull FileStatus status) {
    ContentRevision result;

    if (path.isNonLocal()) {
    // explicitly use local path for deleted items - so these items will be correctly displayed as deleted under local working copy node
    // and not as deleted under remote branch node (in ChangesBrowser)
    // NOTE, that content is still retrieved using remotePath.
      result = SvnRepositoryContentRevision.create(myVcs, path, status == FileStatus.DELETED ? localPath : null, revision.getNumber());
    }
    else {
      result = CurrentContentRevision.create(path);
  }

    return result;
  }

  private static FilePath createFilePath(@NotNull SvnTarget target, boolean isDirectory) {
    return target.isFile()
           ? VcsUtil.getFilePath(target.getFile(), isDirectory)
           : VcsUtil.getFilePathOnNonLocal(SvnUtil.toDecodedString(target), isDirectory);
  }

  @NotNull
  private Change createChange(@NotNull SvnTarget target1, @NotNull SvnTarget target2, @NotNull DiffPath diffPath) throws SvnBindException {
    // TODO: 1) Unify logic of creating Change instance with SvnDiffEditor and SvnChangeProviderContext
    // TODO: 2) If some directory is switched, files inside it are returned as modified in "svn diff --summarize", even if they are equal
    // TODO: to branch files by content - possibly add separate processing of all switched files
    // TODO: 3) Properties change is currently not added as part of result change like in SvnChangeProviderContext.patchWithPropertyChange

    SvnTarget subTarget1 = SvnUtil.append(target1, diffPath.path, true);
    String relativePath = SvnUtil.getRelativeUrl(SvnUtil.toDecodedString(target1), SvnUtil.toDecodedString(subTarget1));

    if (relativePath == null) {
      throw new SvnBindException("Could not get relative path for " + target1 + " and " + subTarget1);
    }

    SvnTarget subTarget2 = SvnUtil.append(target2, FileUtil.toSystemIndependentName(relativePath));

    FilePath target1Path = createFilePath(subTarget1, diffPath.isDirectory());
    FilePath target2Path = createFilePath(subTarget2, diffPath.isDirectory());

    FileStatus status = SvnStatusConvertor
      .convertStatus(SvnStatusHandler.getStatus(diffPath.itemStatus), SvnStatusHandler.getStatus(diffPath.propertiesStatus));

    // statuses determine changes needs to be done to "target1" to get "target2" state
    ContentRevision beforeRevision = status == FileStatus.ADDED
                                     ? null
                                     : createRevision(target1Path, target2Path, target1.getPegRevision(), status);
    ContentRevision afterRevision = status == FileStatus.DELETED
                                    ? null
                                    : createRevision(target2Path, target1Path, target2.getPegRevision(), status);

    return createChange(status, beforeRevision, afterRevision);
  }

  @NotNull
  private static Change createChange(@NotNull final FileStatus status,
                                     @Nullable final ContentRevision beforeRevision,
                                     @Nullable final ContentRevision afterRevision) {
    // isRenamed() and isMoved() are always false here not to have text like "moved from ..." in changes window - by default different
    // paths in before and after revisions are treated as move, but this is not the case for "Compare with Branch"
    return new Change(beforeRevision, afterRevision, status) {
      @Override
      public boolean isRenamed() {
        return false;
      }

      @Override
      public boolean isMoved() {
        return false;
      }
    };
  }

  @XmlRootElement(name = "diff")
  public static class DiffInfo {

    @XmlElementWrapper(name = "paths")
    @XmlElement(name = "path")
    public List<DiffPath> diffPaths = new ArrayList<DiffPath>();
  }

  public static class DiffPath {

    @XmlAttribute(name = "kind", required = true)
    public NodeKind kind;

    @XmlAttribute(name = "props")
    public String propertiesStatus;

    @XmlAttribute(name = "item")
    public String itemStatus;

    @XmlValue
    public String path;

    public boolean isDirectory() {
      return kind.isDirectory();
    }
  }
}