Skip to content

Commit 279e669

Browse files
harrisricvladak
authored andcommitted
Fix svn history for files which were in a directory when the directory was renamed
1 parent db1007f commit 279e669

File tree

4 files changed

+321
-15
lines changed

4 files changed

+321
-15
lines changed

opengrok-indexer/src/main/java/org/opengrok/indexer/history/SubversionHistoryParser.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@
3232
import java.nio.charset.StandardCharsets;
3333
import java.text.ParseException;
3434
import java.util.ArrayList;
35+
import java.util.HashMap;
3536
import java.util.HashSet;
3637
import java.util.List;
38+
import java.util.Map;
39+
import java.util.Map.Entry;
3740
import java.util.Set;
3841
import java.util.logging.Level;
3942
import java.util.logging.Logger;
@@ -64,6 +67,8 @@ class SubversionHistoryParser implements Executor.StreamHandler {
6467

6568
private static class Handler extends DefaultHandler2 {
6669

70+
private static final String COPYFROM_PATH = "copyfrom-path";
71+
6772
/**
6873
* Example of the longest date format that we should accept - SimpleDateFormat cannot cope with micro/nano seconds.
6974
*/
@@ -74,10 +79,12 @@ private static class Handler extends DefaultHandler2 {
7479
final int length;
7580
final List<HistoryEntry> entries = new ArrayList<>();
7681
final Set<String> renamedFiles = new HashSet<>();
82+
final Map<String, String> renamedToDirectoryRevisions = new HashMap<>();
7783
final SubversionRepository repository;
7884
HistoryEntry entry;
7985
StringBuilder sb;
80-
boolean isRenamed;
86+
boolean isRenamedFile;
87+
boolean isRenamedDir;
8188

8289
Handler(String home, String prefix, int length, SubversionRepository repository) {
8390
this.home = home;
@@ -87,19 +94,31 @@ private static class Handler extends DefaultHandler2 {
8794
sb = new StringBuilder();
8895
}
8996

90-
List<String> getRenamedFiles() {
91-
return new ArrayList<>(renamedFiles);
97+
Set<String> getRenamedFiles() {
98+
return renamedFiles;
99+
}
100+
101+
Map<String, String> getRenamedDirectories() {
102+
return renamedToDirectoryRevisions;
92103
}
93104

94105
@Override
95106
public void startElement(String uri, String localName, String qname, Attributes attr) {
96-
isRenamed = false;
107+
isRenamedFile = false;
108+
isRenamedDir = false;
97109
if ("logentry".equals(qname)) {
98110
entry = new HistoryEntry();
99111
entry.setActive(true);
100112
entry.setRevision(attr.getValue("revision"));
101113
} else if ("path".equals(qname)) {
102-
isRenamed = attr.getIndex("copyfrom-path") != -1;
114+
if (attr.getIndex(COPYFROM_PATH) != -1) {
115+
LOGGER.info("rename for:" + attr.getValue(COPYFROM_PATH) );
116+
if ("dir".equals(attr.getValue("kind"))) {
117+
isRenamedDir = true;
118+
} else {
119+
isRenamedFile = true;
120+
}
121+
}
103122
}
104123
sb.setLength(0);
105124
}
@@ -132,9 +151,12 @@ public void endElement(String uri, String localName, String qname) throws SAXExc
132151
// The same file names may be repeated in many commits,
133152
// so intern them to reduce the memory footprint.
134153
entry.addFile(path.intern());
135-
if (isRenamed) {
154+
if (isRenamedFile) {
136155
renamedFiles.add(path.intern());
137156
}
157+
if (isRenamedDir) {
158+
renamedToDirectoryRevisions.put(path.intern(), entry.getRevision());
159+
}
138160
} else {
139161
LOGGER.log(Level.FINER, "Skipping file outside repository: " + s);
140162
}
@@ -209,10 +231,26 @@ History parse(File file, SubversionRepository repos, String sinceRevision,
209231
repos.removeAndVerifyOldestChangeset(entries, sinceRevision);
210232
}
211233

212-
return new History(entries, handler.getRenamedFiles());
234+
Set<String> allRenamedFiles = findRenamedFilesFromDirectories(handler.getRenamedDirectories(), repos, cmdType);
235+
allRenamedFiles.addAll(handler.getRenamedFiles());
236+
237+
return new History(entries, new ArrayList<>(allRenamedFiles));
213238
}
214239

215-
/**
240+
/**
241+
* @return a set of files that were present in the directories at the given revisions.
242+
*/
243+
private Set<String> findRenamedFilesFromDirectories(Map<String, String> renamedDirectories, SubversionRepository repos,
244+
CommandTimeoutType cmdType) {
245+
246+
Set<String> renamedFiles = new HashSet<>();
247+
for (Entry<String, String> entry : renamedDirectories.entrySet()) {
248+
renamedFiles.addAll(repos.getFilesInDirectoryAtRevision(entry.getKey(), entry.getValue(), cmdType));
249+
}
250+
return renamedFiles;
251+
}
252+
253+
/**
216254
* Process the output from the log command and insert the HistoryEntries
217255
* into the history field.
218256
*

opengrok-indexer/src/main/java/org/opengrok/indexer/history/SubversionRepository.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,21 @@
2323
*/
2424
package org.opengrok.indexer.history;
2525

26+
import java.io.BufferedReader;
2627
import java.io.File;
2728
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.io.InputStreamReader;
2831
import java.io.OutputStream;
32+
import java.nio.charset.StandardCharsets;
2933
import java.util.ArrayList;
34+
import java.util.HashSet;
3035
import java.util.List;
36+
import java.util.Set;
3137
import java.util.function.Supplier;
3238
import java.util.logging.Level;
3339
import java.util.logging.Logger;
40+
3441
import javax.xml.XMLConstants;
3542
import javax.xml.parsers.DocumentBuilder;
3643
import javax.xml.parsers.DocumentBuilderFactory;
@@ -41,6 +48,7 @@
4148
import org.opengrok.indexer.configuration.RuntimeEnvironment;
4249
import org.opengrok.indexer.logger.LoggerFactory;
4350
import org.opengrok.indexer.util.Executor;
51+
import org.opengrok.indexer.util.Executor.StreamHandler;
4452
import org.opengrok.indexer.util.LazilyInstantiate;
4553
import org.w3c.dom.Document;
4654
import org.w3c.dom.Node;
@@ -249,7 +257,7 @@ boolean getHistoryGet(OutputStream out, String parent, String basename, String r
249257

250258
String filepath;
251259
try {
252-
filepath = (new File(parent, basename)).getCanonicalPath();
260+
filepath = new File(parent, basename).getCanonicalPath();
253261
} catch (IOException exp) {
254262
LOGGER.log(Level.SEVERE,
255263
"Failed to get canonical path: {0}", exp.getClass().toString());
@@ -282,6 +290,73 @@ boolean getHistoryGet(OutputStream out, String parent, String basename, String r
282290
return false;
283291
}
284292

293+
/**
294+
* Get an executor to be used for retrieving the history log for the named
295+
* file.
296+
*
297+
* @param dir The repository relative directory to retrieve file list for
298+
* @param atRevision the revision number at which we want the directory contents
299+
* @param cmdType command timeout type
300+
* @return An Executor ready to be started
301+
*/
302+
private Executor getDirectoryListExecutor(final String dir, String atRevision, CommandTimeoutType cmdType) {
303+
304+
List<String> cmd = new ArrayList<>();
305+
ensureCommand(CMD_PROPERTY_KEY, CMD_FALLBACK);
306+
cmd.add(RepoCommand);
307+
cmd.add("ls");
308+
cmd.add(escapeFileName(dir));
309+
cmd.add("-r" + atRevision);
310+
311+
return new Executor(cmd, new File(getDirectoryName()),
312+
RuntimeEnvironment.getInstance().getCommandTimeout(cmdType));
313+
}
314+
315+
316+
/**
317+
* Provides a list of files that were in a directory at a given revision.
318+
* This is useful for finding files that will need special renamed file history
319+
* handling because they were in a directory when it was renamed.
320+
*
321+
* Note that this doesn't throw an exception even if the command was not completed
322+
* because we will still be able to get the file history up to this point.
323+
*
324+
* @param directory the directory to check
325+
* @param revision the revision to check at
326+
* @param cmdType the timeout setting.
327+
* @return the files that were in the directory at that revision
328+
*/
329+
Set<String> getFilesInDirectoryAtRevision(String directory, String revision,
330+
CommandTimeoutType cmdType) {
331+
332+
Executor executor = getDirectoryListExecutor(
333+
RuntimeEnvironment.getInstance().getSourceRootPath() + File.separator + directory,
334+
revision, cmdType);
335+
336+
Set<String> files = new HashSet<>();
337+
338+
StreamHandler directoryLogStreamHandler = new StreamHandler() {
339+
340+
@Override
341+
public void processStream(InputStream in) throws IOException {
342+
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
343+
.lines()
344+
.filter(s -> !s.isBlank())
345+
.map(s -> directory + File.separator + s)
346+
.forEach(files::add);
347+
}
348+
};
349+
350+
int status = executor.exec(true, directoryLogStreamHandler);
351+
if (status != 0) {
352+
LOGGER.warning("Failed to get history for: \"" + directory + "\" Exit code: " + status);
353+
}
354+
LOGGER.info("log from directory / revision [" + directory + "] [" + revision + "]\n" + files);
355+
356+
return files;
357+
}
358+
359+
285360
@Override
286361
boolean hasHistoryForDirectories() {
287362
return true;

opengrok-indexer/src/test/java/org/opengrok/indexer/history/FileHistoryCacheTest.java

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.LinkedList;
4949
import java.util.List;
5050
import java.util.Map;
51+
import java.util.Set;
5152

5253
import org.apache.commons.lang3.time.DateUtils;
5354
import org.eclipse.jgit.api.Git;
@@ -60,6 +61,7 @@
6061
import org.junit.jupiter.params.provider.ValueSource;
6162
import org.mockito.Mockito;
6263
import org.opengrok.indexer.condition.EnabledForRepository;
64+
import org.opengrok.indexer.configuration.CommandTimeoutType;
6365
import org.opengrok.indexer.configuration.Filter;
6466
import org.opengrok.indexer.configuration.IgnoredNames;
6567
import org.opengrok.indexer.configuration.RuntimeEnvironment;
@@ -73,6 +75,8 @@
7375
*/
7476
class FileHistoryCacheTest {
7577

78+
private static final String SUBVERSION_REPO_LOC = "subversion";
79+
7680
private static final String SVN_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
7781

7882
private final RuntimeEnvironment env = RuntimeEnvironment.getInstance();
@@ -633,7 +637,7 @@ void testRenamedFilePlusChangesBranched() throws Exception {
633637
void testMultipleRenamedFiles() throws Exception {
634638
createSvnRepository();
635639

636-
File reposRoot = new File(repositories.getSourceRoot(), "subversion");
640+
File reposRoot = new File(repositories.getSourceRoot(), SUBVERSION_REPO_LOC);
637641
History updatedHistory;
638642

639643
// The test expects support for renamed files.
@@ -645,7 +649,7 @@ void testMultipleRenamedFiles() throws Exception {
645649
cache.store(historyToStore, repo);
646650

647651
// Check complete list of history entries for the renamed file.
648-
File testFile = new File(reposRoot.toString() + File.separatorChar + "FileZ.txt");
652+
File testFile = new File(reposRoot.toString(), "FileZ.txt");
649653
updatedHistory = cache.get(testFile, repo, false);
650654
assertEquals(3, updatedHistory.getHistoryEntries().size());
651655

@@ -677,6 +681,82 @@ void testMultipleRenamedFiles() throws Exception {
677681
assertSameEntries(histConstruct.getHistoryEntries(), updatedHistory.getHistoryEntries(), false);
678682
}
679683

684+
685+
/**
686+
* For an Subversion repository, verify we can get a list of files that were in a directory
687+
* at the point it was renamed.
688+
*/
689+
@EnabledForRepository(SUBVERSION)
690+
@Test
691+
void testSubversionFilesInDirectoryLog() throws Exception {
692+
createSvnRepository();
693+
694+
File reposRoot = new File(repositories.getSourceRoot(), SUBVERSION_REPO_LOC);
695+
696+
// The test expects support for renamed files.
697+
env.setHandleHistoryOfRenamedFiles(true);
698+
699+
// Generate history index.
700+
SubversionRepository svnRepo = (SubversionRepository) RepositoryFactory.getRepository(reposRoot);
701+
Set<String> files = svnRepo.getFilesInDirectoryAtRevision(
702+
Path.of(SUBVERSION_REPO_LOC, "renamedFolder").toString(), "12", CommandTimeoutType.INDEXER);
703+
assertEquals(
704+
Set.of(Path.of(SUBVERSION_REPO_LOC, "renamedFolder", "FileInRenamedFolder.txt").toString()), files);
705+
}
706+
707+
708+
/**
709+
* Make sure produces correct history for a file which was in a directory that was renamed.
710+
*/
711+
@EnabledForRepository(SUBVERSION)
712+
@Test
713+
void testRenamedDirectory() throws Exception {
714+
createSvnRepository();
715+
716+
File reposRoot = new File(repositories.getSourceRoot(), SUBVERSION_REPO_LOC);
717+
History updatedHistory;
718+
719+
// The test expects support for renamed files.
720+
env.setHandleHistoryOfRenamedFiles(true);
721+
722+
// Generate history index.
723+
Repository repo = RepositoryFactory.getRepository(reposRoot);
724+
History historyToStore = repo.getHistory(reposRoot);
725+
cache.store(historyToStore, repo);
726+
727+
// Check complete list of history entries for the file in the renamed folder.
728+
File testFile = Path.of(reposRoot.toString(), "renamedFolder", "FileInRenamedFolder.txt").toFile();
729+
updatedHistory = cache.get(testFile, repo, false);
730+
assertEquals(3, updatedHistory.getHistoryEntries().size());
731+
732+
HistoryEntry e0 = new HistoryEntry(
733+
"14",
734+
DateUtils.parseDate("2021-03-02T11:34:28.030Z", SVN_DATE_FORMAT),
735+
"RichardH",
736+
"Update to renamed file.",
737+
true);
738+
HistoryEntry e1 = new HistoryEntry(
739+
"13",
740+
DateUtils.parseDate("2021-03-02T08:54:54.693Z", SVN_DATE_FORMAT),
741+
"RichardH",
742+
"rename folder",
743+
true);
744+
HistoryEntry e2 = new HistoryEntry(
745+
"12",
746+
DateUtils.parseDate("2021-03-02T08:54:30.615Z", SVN_DATE_FORMAT),
747+
"RichardH",
748+
"added file to new folder",
749+
true);
750+
751+
History histConstruct = new History();
752+
LinkedList<HistoryEntry> entriesConstruct = new LinkedList<>();
753+
entriesConstruct.add(e0);
754+
entriesConstruct.add(e1);
755+
entriesConstruct.add(e2);
756+
histConstruct.setHistoryEntries(entriesConstruct);
757+
assertSameEntries(histConstruct.getHistoryEntries(), updatedHistory.getHistoryEntries(), false);
758+
}
759+
680760
private void createSvnRepository() throws Exception {
681761
var svnLog = FileHistoryCacheTest.class.getResource("/history/svnlog.dump");
682762
Path tempDir = Files.createTempDirectory("opengrok");
@@ -697,7 +777,7 @@ private void createSvnRepository() throws Exception {
697777
assertEquals(0, svnLoadRepoFromDumpProcess.waitFor());
698778

699779
var svnCheckoutProcess = new ProcessBuilder("svn", "checkout", Path.of(repo).toUri().toString(),
700-
Path.of(repositories.getSourceRoot()).resolve("subversion").toString())
780+
Path.of(repositories.getSourceRoot()).resolve(SUBVERSION_REPO_LOC).toString())
701781
.start();
702782
assertEquals(0, svnCheckoutProcess.waitFor());
703783
}
@@ -784,7 +864,7 @@ void testRenamedFileHistoryWithPerPartes(int maxCount) throws Exception {
784864
void testRenamedFile() throws Exception {
785865
createSvnRepository();
786866

787-
File reposRoot = new File(repositories.getSourceRoot(), "subversion");
867+
File reposRoot = new File(repositories.getSourceRoot(), SUBVERSION_REPO_LOC);
788868
History updatedHistory;
789869

790870
// The test expects support for renamed files.
@@ -796,8 +876,7 @@ void testRenamedFile() throws Exception {
796876
cache.store(historyToStore, repo);
797877

798878
// Check complete list of history entries for the renamed file.
799-
File testFile = new File(reposRoot.toString() + File.separatorChar
800-
+ "subfolder" + File.separatorChar + "TestFileRenamedAgain.txt");
879+
File testFile = Path.of(reposRoot.toString(), "subfolder", "TestFileRenamedAgain.txt").toFile();
801880
updatedHistory = cache.get(testFile, repo, false);
802881
assertEquals(4, updatedHistory.getHistoryEntries().size());
803882

0 commit comments

Comments
 (0)