Skip to content

Commit 9b403e5

Browse files
committed
Merge pull request #14 from clue-labs/logs
Add Client::fetchLog()
2 parents 904a293 + 6abbfb9 commit 9b403e5

File tree

8 files changed

+1357
-0
lines changed

8 files changed

+1357
-0
lines changed

src/Client.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@ public function fetchPatch($path, $r1, $r2)
9191
return $this->fetch($url);
9292
}
9393

94+
public function fetchLog($path, $revision = null)
95+
{
96+
$url = $path . '?view=log';
97+
98+
// TODO: invalid revision shows error page, but HTTP 200 OK
99+
100+
if ($revision !== null) {
101+
$url .= (strpos($url, '?') === false) ? '?' : '&';
102+
$url .= 'pathrev=' . $revision;
103+
}
104+
105+
return $this->fetchXml($url)->then(array($this->parser, 'parseLogEntries'));
106+
}
107+
94108
public function fetchRevisionPrevious($path, $revision)
95109
{
96110
return $this->fetchAllPreviousRevisions($path)->then(function ($revisions) use ($revision) {

src/Io/Parser.php

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,105 @@ public function parseLogRevisions(SimpleXMLElement $xml)
5252
return $revisions;
5353
}
5454

55+
/**
56+
* Parse log entries from given XML document
57+
*
58+
* @param SimpleXMLElement $xml
59+
* @throws \UnexpectedValueException
60+
* @return array
61+
* @link https://gforge.inria.fr/scm/viewvc/viewvc.org/template-authoring-guide.html#variables-log
62+
*/
63+
public function parseLogEntries(SimpleXMLElement $xml)
64+
{
65+
$entries = array();
66+
67+
foreach ($xml->xpath('//div[pre]') as $div) {
68+
/* @var $div SimpleXMLElement */
69+
70+
// skip "(vendor branch)" "em" tag if found
71+
$off = ((string)$div->em[0] === '(vendor branch)') ? 1 : 0;
72+
73+
$entry = array(
74+
// revision is first "strong" element (subversion wraps this in "a" element)
75+
'revision' => (string)$this->first($div->xpath('.//strong[1]')),
76+
// date is in first "em" element
77+
'date' => new \DateTime((string)$div->em[0 + $off]),
78+
// author is in second "em" element
79+
'author' => (string)$div->em[1 + $off],
80+
// message is in only "pre" element
81+
'message' => (string)$div->pre
82+
);
83+
84+
// ease parsing each line by splitting on "br" element, skip static rows for revision/date
85+
$parts = explode('<br />', substr($div->asXML(), 5, -6));
86+
unset($parts[0], $parts[1]);
87+
88+
foreach ($parts as $part) {
89+
$part = new SimpleXMLElement('<body>' . $part . '</body>');
90+
$str = (string)$part;
91+
92+
if (substr($str, 0, 7) === 'Diff to') {
93+
$value = array();
94+
95+
foreach ($part->xpath('.//a') as $a) {
96+
$text = (string)$a;
97+
$pos = strrpos($text, ' ');
98+
99+
// text should be "previous X.Y", otherwise ignore "(colored)" with no blank
100+
if ($pos !== false) {
101+
$value[substr($text, 0, $pos)] = substr($text, $pos + 1);
102+
}
103+
}
104+
105+
$entry['diff'] = $value;
106+
} elseif (substr($str, 0, 7) === 'Branch:' || substr($str, 0, 9) === 'CVS Tags:' || substr($str, 0, 17) === 'Branch point for:') {
107+
$value = array();
108+
109+
foreach ($part->xpath('.//a/strong') as $a) {
110+
$value []= (string)$a;
111+
}
112+
113+
$key = $str[0] === 'B' ? ($str[6] === ':' ? 'branches' : 'branchpoints') : 'tags';
114+
$entry[$key] = $value;
115+
} elseif (substr($str, 0, 13) === 'Changes since') {
116+
// "strong" element contains "X.Y: +1 -2 lines"
117+
$value = (string)$part->strong;
118+
$pos = strpos($value, ':');
119+
120+
// previous revision is before colon
121+
$entry['previous'] = substr($value, 0, $pos);
122+
123+
// changes are behind colon
124+
$entry['changes'] = substr($value, $pos + 2);
125+
} elseif (substr($str, 0, 14) === 'Original Path:') {
126+
$entry['original'] = (string)$part->a->em;
127+
} elseif (substr($str, 0, 12) === 'File length:') {
128+
$entry['size'] = (int)substr($str, 13);
129+
} elseif (isset($part->strong->em) && (string)$part->strong->em === 'FILE REMOVED') {
130+
$entry['deleted'] = true;
131+
}
132+
}
133+
134+
// previous is either set via "changes since" or link to "diff to" previous
135+
if (isset($entry['diff']['previous'])) {
136+
$entry['previous'] = $entry['diff']['previous'];
137+
}
138+
139+
if ($off) {
140+
$entry['vendor'] = true;
141+
}
142+
143+
$entries []= $entry;
144+
}
145+
146+
return $entries;
147+
}
148+
149+
private function first(array $a)
150+
{
151+
return $a[0];
152+
}
153+
55154
private function linkParameters($href)
56155
{
57156
$args = array();

tests/ClientTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ public function testFetchDirectoryRevisionAttic()
8585
$this->expectPromiseReject($promise);
8686
}
8787

88+
public function testFetchLogRevision()
89+
{
90+
$this->browser->expects($this->once())->method('get')->with($this->equalTo('http://viewvc.example.org/README.md?view=log&pathrev=1.0'))->will($this->returnValue($this->createPromiseRejected()));
91+
92+
$promise = $this->client->fetchLog('/README.md', '1.0');
93+
94+
$this->expectPromiseReject($promise);
95+
}
96+
8897
public function testFetchPatch()
8998
{
9099
$this->browser->expects($this->once())->method('get')->with($this->equalTo('http://viewvc.example.org/README.md?view=patch&r1=1.0&r2=1.1'))->will($this->returnValue($this->createPromiseRejected()));

tests/Io/ParserTest.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,71 @@ public function testDirectoryListing()
3232
$this->assertEquals(array('images/', 'stylesheets/', 'index.xml', 'velocity.properties'), $files);
3333
}
3434

35+
public function testLogSubversion()
36+
{
37+
$xml = $this->loadXml('is-a-file.html');
38+
39+
$log = $this->parser->parseLogEntries($xml);
40+
41+
$this->assertCount(3, $log);
42+
43+
// second entry has previous
44+
$entry = $log[1];
45+
$this->assertEquals('168695', $entry['revision']);
46+
$this->assertEquals(new DateTime('Tue Jun 22 02:15:08 1999 UTC'), $entry['date']);
47+
$this->assertEquals('(unknown author)', $entry['author']);
48+
$this->assertEquals('168694', $entry['previous']);
49+
$this->assertEquals('jakarta/ecs/branches/ecs/src/java/org/apache/ecs/AlignType.java', $entry['original']);
50+
$this->assertEquals(4131, $entry['size']);
51+
52+
// last entry has no previous
53+
$entry = $log[2];
54+
$this->assertEquals('168694', $entry['revision']);
55+
$this->assertEquals(new DateTime('Tue Jun 22 02:15:08 1999 UTC'), $entry['date']);
56+
$this->assertEquals('jonbolt', $entry['author']);
57+
$this->assertFalse(isset($entry['previous']));
58+
}
59+
60+
public function testLogCvsBranches()
61+
{
62+
$xml = $this->loadXml('log-file-cvs-branches.html');
63+
64+
$log = $this->parser->parseLogEntries($xml);
65+
66+
$this->assertCount(4, $log);
67+
68+
// second entry is on vendor branch
69+
$entry = $log[1];
70+
$this->assertEquals('1.1.1.2', $entry['revision']);
71+
$this->assertEquals(new DateTime('Tue Nov 5 05:48:37 2002 UTC'), $entry['date']);
72+
$this->assertEquals('abcosico', $entry['author']);
73+
$this->assertEquals('1.1.1.1', $entry['previous']);
74+
$this->assertEquals(array('ICIS', 'IRRI', 'MAIN', 'avendor', 'bbu'), $entry['branches']);
75+
$this->assertEquals(array('arelease', 'v1'), $entry['tags']);
76+
$this->assertTrue(isset($entry['vendor']));
77+
78+
// last entry is branchpoint for all branches and is not on vendor branch
79+
$entry = $log[3];
80+
$this->assertEquals('1.1', $entry['revision']);
81+
$this->assertEquals(array('ICIS', 'IRRI', 'MAIN', 'avendor', 'bbu'), $entry['branchpoints']);
82+
$this->assertFalse(isset($entry['vendor']));
83+
}
84+
85+
public function testLogCvsDeleted()
86+
{
87+
$xml = $this->loadXml('log-file-cvs-deleted.html');
88+
89+
$log = $this->parser->parseLogEntries($xml);
90+
91+
$this->assertCount(6, $log);
92+
93+
// second entry is a delete
94+
$entry = $log[1];
95+
$this->assertEquals('1.5', $entry['revision']);
96+
$this->assertEquals('1.4', $entry['previous']);
97+
$this->assertTrue($entry['deleted']);
98+
}
99+
35100
private function loadXml($file)
36101
{
37102
return $this->loader->loadXmlFile(__DIR__ . '/../fixtures/' . $file);

0 commit comments

Comments
 (0)