Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/Changelog.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,45 @@ public function __construct(Configuration $config)
{
$this->config = $config;
$this->remote = Repository::parseRemoteUrl();

// Auto-configure Azure DevOps URL formats
if ($this->isAzureDevOps()) {
$this->configureAzureDevOpsUrls();
}
}

/**
* Check if remote is Azure DevOps.
*/
protected function isAzureDevOps(): bool
{
return isset($this->remote['host']) &&
(strpos($this->remote['host'], 'dev.azure.com') !== false ||
strpos($this->remote['host'], 'ssh.dev.azure.com') !== false);
}

/**
* Configure URL formats for Azure DevOps.
*/
protected function configureAzureDevOpsUrls(): void
{
// Normalize host for SSH URLs (ssh.dev.azure.com -> dev.azure.com)
if (isset($this->remote['host']) && $this->remote['host'] === 'ssh.dev.azure.com') {
$this->remote['host'] = 'dev.azure.com';
}

// Azure DevOps uses a different URL structure
// Commit: https://dev.azure.com/{org}/{project}/_git/{repo}/commit/{hash}
$this->config->setCommitUrlFormat('{{host}}/{{owner}}/{{project}}/_git/{{repository}}/commit/{{hash}}');

// Compare: https://dev.azure.com/{org}/{project}/_git/{repo}/branchCompare?baseVersion=GT{base}&targetVersion=GT{target}
$this->config->setCompareUrlFormat('{{host}}/{{owner}}/{{project}}/_git/{{repository}}/branchCompare?baseVersion=GT{{previousTag}}&targetVersion=GT{{currentTag}}');

// Issue/Work Item: https://dev.azure.com/{org}/{project}/_workitems/edit/{id}
$this->config->setIssueUrlFormat('{{host}}/{{owner}}/{{project}}/_workitems/edit/{{id}}');

// User: https://dev.azure.com/{org}/_user/{user}
$this->config->setUserUrlFormat('{{host}}/{{owner}}/_user/{{user}}');
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/Git/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ public static function parseRemoteUrl()
{
$url = self::getRemoteUrl();
$patterns = [
// Azure DevOps HTTPS: https://dev.azure.com/{organization}/{project}/_git/{repository}
'#^(?P<protocol>https?)://(?P<host>dev\.azure\.com)/(?P<owner>[^/]+)/(?P<project>[^/]+)/_git/(?P<repository>[^/]+?)(?:\.git)?/?$#smi',
// Azure DevOps SSH: git@ssh.dev.azure.com:v3/{organization}/{project}/{repository}
'#^(?P<user>[^@]+)@(?P<host>ssh\.dev\.azure\.com):v3/(?P<owner>[^/]+)/(?P<project>[^/]+)/(?P<repository>[^/]+?)(?:\.git)?/?$#smi',
'#^(?P<protocol>https?|git|ssh|rsync)\://(?:(?P<user>.+)@)*(?P<host>[a-z0-9_.-]*)[:/]*(?P<port>[\d]+){0,1}(?P<pathname>\/((?P<owner>.+)\/)?((?P<repository>.+?)(\.git|\/)?)?)$#smi',
'#(git\+)?((?P<protocol>\w+)://)((?P<user>\w+)@)?((?P<host>[\w\.\-]+))(:(?P<port>\d+))?(?P<pathname>(\/(?P<owner>.+)/)?(\/?(?P<repository>.+)(\.git|\/)?)?)$#smi',
'#^(?:(?P<user>.+)@)*(?P<host>[a-z0-9_.-]*)[:]*(?P<port>[\d]+){0,1}(?P<pathname>\/?(?P<owner>.+)/(?P<repository>.+).git)$#smi',
Expand Down
124 changes: 124 additions & 0 deletions tests/ChangelogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,128 @@ public function testExecMock()
$output = shell_exec('bar');
$this->assertEquals('foo', $output);
}

/** @test */
public function testAzureDevOpsHttpsUrlPatternMatching()
{
// Test Azure DevOps HTTPS URL pattern directly
$url = 'https://dev.azure.com/myorg/myproject/_git/myrepo';
$pattern = '#^(?P<protocol>https?)://(?P<host>dev\.azure\.com)/(?P<owner>[^/]+)/(?P<project>[^/]+)/_git/(?P<repository>[^/]+?)(?:\.git)?/?$#smi';

$this->assertEquals(1, preg_match($pattern, $url, $match));
$result = array_filter($match, 'is_string', ARRAY_FILTER_USE_KEY);

$this->assertEquals('dev.azure.com', $result['host']);
$this->assertEquals('myorg', $result['owner']);
$this->assertEquals('myproject', $result['project']);
$this->assertEquals('myrepo', $result['repository']);
}

/** @test */
public function testAzureDevOpsSshUrlPatternMatching()
{
// Test Azure DevOps SSH URL pattern directly
$url = 'git@ssh.dev.azure.com:v3/myorg/myproject/myrepo';
$pattern = '#^(?P<user>[^@]+)@(?P<host>ssh\.dev\.azure\.com):v3/(?P<owner>[^/]+)/(?P<project>[^/]+)/(?P<repository>[^/]+?)(?:\.git)?/?$#smi';

$this->assertEquals(1, preg_match($pattern, $url, $match));
$result = array_filter($match, 'is_string', ARRAY_FILTER_USE_KEY);

$this->assertEquals('ssh.dev.azure.com', $result['host']);
$this->assertEquals('myorg', $result['owner']);
$this->assertEquals('myproject', $result['project']);
$this->assertEquals('myrepo', $result['repository']);
}

/** @test */
public function testAzureDevOpsDetection()
{
// Use reflection to test the isAzureDevOps method
$config = new Configuration();
$changelog = new Changelog($config);

$class = new \ReflectionClass($changelog);
$remoteProperty = $class->getProperty('remote');
$remoteProperty->setAccessible(true);

$method = $class->getMethod('isAzureDevOps');
$method->setAccessible(true);

// Test with Azure DevOps host
$remoteProperty->setValue($changelog, ['host' => 'dev.azure.com']);
$this->assertTrue($method->invoke($changelog));

$remoteProperty->setValue($changelog, ['host' => 'ssh.dev.azure.com']);
$this->assertTrue($method->invoke($changelog));

// Test with non-Azure host
$remoteProperty->setValue($changelog, ['host' => 'github.com']);
$this->assertFalse($method->invoke($changelog));
}

/** @test */
public function testAzureDevOpsUrlConfiguration()
{
// Test that Azure DevOps URL formats are configured correctly
$config = new Configuration();
$changelog = new Changelog($config);

$class = new \ReflectionClass($changelog);
$remoteProperty = $class->getProperty('remote');
$remoteProperty->setAccessible(true);

$method = $class->getMethod('configureAzureDevOpsUrls');
$method->setAccessible(true);

// Set Azure remote
$remoteProperty->setValue($changelog, [
'host' => 'dev.azure.com',
'owner' => 'myorg',
'project' => 'myproject',
'repository' => 'myrepo',
]);

// Apply Azure configuration
$method->invoke($changelog);

// Verify Azure DevOps URL formats were applied
$this->assertStringContainsString('branchCompare', $config->getCompareUrlFormat());
$this->assertStringContainsString('baseVersion=GT', $config->getCompareUrlFormat());
$this->assertStringContainsString('targetVersion=GT', $config->getCompareUrlFormat());
$this->assertStringContainsString('{{project}}', $config->getCompareUrlFormat());
$this->assertStringContainsString('_git', $config->getCommitUrlFormat());
$this->assertStringContainsString('_workitems', $config->getIssueUrlFormat());
}

/** @test */
public function testAzureDevOpsHostNormalization()
{
// Test that SSH host is normalized to dev.azure.com
$config = new Configuration();
$changelog = new Changelog($config);

$class = new \ReflectionClass($changelog);
$remoteProperty = $class->getProperty('remote');
$remoteProperty->setAccessible(true);

$method = $class->getMethod('configureAzureDevOpsUrls');
$method->setAccessible(true);

// Set Azure SSH remote
$remoteProperty->setValue($changelog, [
'host' => 'ssh.dev.azure.com',
'owner' => 'myorg',
'project' => 'myproject',
'repository' => 'myrepo',
]);

// Apply Azure configuration
$method->invoke($changelog);

// Get the updated remote
$remote = $remoteProperty->getValue($changelog);

// Verify host was normalized to dev.azure.com
$this->assertEquals('dev.azure.com', $remote['host']);
}
}
Loading