Skip to content

Commit 49a0b64

Browse files
authored
remove azdo git remote constraint for dev.azure.com only (#4104)
* support legacy hostname for azdo * test updates * simplify with no regex
1 parent 4707076 commit 49a0b64

File tree

2 files changed

+170
-56
lines changed

2 files changed

+170
-56
lines changed

cli/azd/pkg/pipeline/azdo_provider.go

Lines changed: 51 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"log"
11-
"regexp"
1211
"strings"
1312

1413
"github.com/azure/azure-dev/cli/azd/pkg/azdo"
@@ -418,56 +417,68 @@ func (p *AzdoScmProvider) promptForAzdoRepository(ctx context.Context, console i
418417
return remoteUrl, nil
419418
}
420419

421-
// defines the structure of an ssl git remote
422-
var azdoRemoteGitUrlRegex = regexp.MustCompile(`^git@ssh.dev.azure\.com:(.*?)(?:\.git)?$`)
423-
424-
// defines the structure of an HTTPS git remote
425-
var azdoRemoteHttpsUrlRegex = regexp.MustCompile(`^https://[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*:*.+@dev.azure\.com/(.*?)$`)
426-
427420
// ErrRemoteHostIsNotAzDo the error used when a non Azure DevOps remote is found
428421
var ErrRemoteHostIsNotAzDo = errors.New("existing remote is not an Azure DevOps host")
429422

430423
// ErrSSHNotSupported the error used when ssh git remote is detected
431424
var ErrSSHNotSupported = errors.New("ssh git remote is not supported. " +
432425
"Use HTTPS git remote to connect the remote repository")
433426

434-
// helper function to determine if the provided remoteUrl is an azure devops repo.
435-
// currently supports AzDo PaaS
436-
func isAzDoRemote(remoteUrl string) error {
437-
if azdoRemoteGitUrlRegex.MatchString(remoteUrl) {
438-
return ErrSSHNotSupported
439-
}
440-
slug := ""
441-
for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} {
442-
captures := r.FindStringSubmatch(remoteUrl)
443-
if captures != nil {
444-
slug = captures[1]
427+
type azdoRemote struct {
428+
Project string
429+
RepositoryName string
430+
}
431+
432+
// parseAzDoRemote extracts the organization, project and repository name from an Azure DevOps remote url
433+
// the url can be in the form of:
434+
// - https://dev.azure.com/[org|user]/[project]/_git/[repo]
435+
// - https://[user]@dev.azure.com/[org|user]/[project]/_git/[repo]
436+
// - https://[org].visualstudio.com/[project]/_git/[repo]
437+
// - git@ssh.dev.azure.com:v[1-3]/[user|org]/[project]/[repo]
438+
// - git@vs-ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
439+
// - git@ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
440+
func parseAzDoRemote(remoteUrl string) (*azdoRemote, error) {
441+
// Initialize the azdoRemote struct
442+
azdoRemote := &azdoRemote{}
443+
444+
if !strings.Contains(remoteUrl, "visualstudio.com") && !strings.Contains(remoteUrl, "dev.azure.com") {
445+
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
446+
}
447+
448+
if strings.Contains(remoteUrl, "/_git/") {
449+
// applies to http or https
450+
parts := strings.Split(remoteUrl, "/_git/")
451+
projectNameStart := strings.LastIndex(parts[0], "/")
452+
projectPartLen := len(parts[0])
453+
454+
if len(parts) != 2 || // remoteUrl must have exactly one "/_git/" substring
455+
!strings.Contains(parts[0], "/") || // part 0 (the project) must have more than one "/"
456+
projectPartLen <= 1 || // part 0 must be greater than 1 character
457+
projectNameStart == projectPartLen-1 { // part 0 must not end with "/"
458+
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
445459
}
460+
461+
azdoRemote.Project = parts[0][projectNameStart+1:]
462+
azdoRemote.RepositoryName = parts[1]
463+
return azdoRemote, nil
446464
}
447-
if slug == "" {
448-
return ErrRemoteHostIsNotAzDo
449-
}
450-
return nil
451-
}
452465

453-
func parseAzDoRemote(remoteUrl string) (string, error) {
454-
for _, r := range []*regexp.Regexp{azdoRemoteGitUrlRegex, azdoRemoteHttpsUrlRegex} {
455-
captures := r.FindStringSubmatch(remoteUrl)
456-
if captures != nil {
457-
return captures[1], nil
458-
}
466+
if strings.Contains(remoteUrl, "git@") {
467+
// applies to git@ -> project and repo always in the last two parts
468+
parts := strings.Split(remoteUrl, "/")
469+
partsLen := len(parts)
470+
azdoRemote.Project = parts[partsLen-2]
471+
azdoRemote.RepositoryName = parts[partsLen-1]
472+
return azdoRemote, nil
459473
}
460-
return "", nil
474+
475+
// If the remoteUrl does not match any of the supported formats, return an error
476+
return nil, fmt.Errorf("%w: %s", ErrRemoteHostIsNotAzDo, remoteUrl)
461477
}
462478

463479
// gitRepoDetails extracts the information from an Azure DevOps remote url into general scm concepts
464480
// like owner, name and path
465481
func (p *AzdoScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string) (*gitRepositoryDetails, error) {
466-
err := isAzDoRemote(remoteUrl)
467-
if err != nil {
468-
return nil, err
469-
}
470-
471482
repoDetails := p.getRepoDetails()
472483
// Try getting values from the env.
473484
// This is a quick shortcut to avoid parsing the remote in detail.
@@ -496,17 +507,16 @@ func (p *AzdoScmProvider) gitRepoDetails(ctx context.Context, remoteUrl string)
496507
}
497508

498509
if repoDetails.projectId == "" || repoDetails.repoId == "" {
499-
// Removing environment or creating a new one would remove any memory fro project
510+
// Removing environment or creating a new one would remove any memory from project
500511
// and repo. In that case, it needs to be calculated from the remote url
501-
azdoSlug, err := parseAzDoRemote(remoteUrl)
512+
azdoRemote, err := parseAzDoRemote(remoteUrl)
502513
if err != nil {
503514
return nil, fmt.Errorf("parsing Azure DevOps remote url: %s: %w", remoteUrl, err)
504515
}
505-
// azdoSlug => Org/Project/_git/repoName
506-
parts := strings.Split(azdoSlug, "_git/")
507-
repoDetails.projectName = strings.Split(parts[0], "/")[1]
516+
517+
repoDetails.projectName = azdoRemote.Project
508518
p.env.DotenvSet(azdo.AzDoEnvironmentProjectName, repoDetails.projectName)
509-
repoDetails.repoName = parts[1]
519+
repoDetails.repoName = azdoRemote.RepositoryName
510520
p.env.DotenvSet(azdo.AzDoEnvironmentRepoName, repoDetails.repoName)
511521

512522
connection, err := p.getAzdoConnection(ctx)

cli/azd/pkg/pipeline/azdo_provider_test.go

Lines changed: 119 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,11 @@ func Test_azdo_provider_getRepoDetails(t *testing.T) {
3838
require.EqualValues(t, false, details.pushStatus)
3939
})
4040

41-
t.Run("ssh not supported", func(t *testing.T) {
42-
// arrange
43-
provider := getAzdoScmProviderTestHarness(mockinput.NewMockConsole())
44-
ctx := context.Background()
45-
46-
// act
47-
details, e := provider.gitRepoDetails(ctx, "git@ssh.dev.azure.com:v3/fake_org/repo1/repo1")
48-
49-
// assert
50-
require.Error(t, e, ErrSSHNotSupported)
51-
require.EqualValues(t, (*gitRepositoryDetails)(nil), details)
52-
})
53-
5441
t.Run("non azure devops https remote", func(t *testing.T) {
5542
//arrange
56-
provider := &AzdoScmProvider{}
43+
provider := &AzdoScmProvider{
44+
env: environment.New("test"),
45+
}
5746
ctx := context.Background()
5847

5948
//act
@@ -66,7 +55,9 @@ func Test_azdo_provider_getRepoDetails(t *testing.T) {
6655

6756
t.Run("non azure devops git remote", func(t *testing.T) {
6857
//arrange
69-
provider := &AzdoScmProvider{}
58+
provider := &AzdoScmProvider{
59+
env: environment.New("test"),
60+
}
7061
ctx := context.Background()
7162

7263
//act
@@ -221,3 +212,116 @@ func getAzdoCiProviderTestHarness(console input.Console) *AzdoCiProvider {
221212
console: console,
222213
}
223214
}
215+
216+
func Test_parseAzDoRemote(t *testing.T) {
217+
218+
// the url can be in the form of:
219+
// - https://dev.azure.com/[org|user]/[project]/_git/[repo]
220+
t.Run("valid HTTPS remote", func(t *testing.T) {
221+
remoteUrl := "https://dev.azure.com/org/project/_git/repo"
222+
expected := &azdoRemote{
223+
Project: "project",
224+
RepositoryName: "repo",
225+
}
226+
227+
result, err := parseAzDoRemote(remoteUrl)
228+
229+
require.NoError(t, err)
230+
require.Equal(t, expected, result)
231+
})
232+
233+
// the url can be in the form of:
234+
// - https://[user]@dev.azure.com/[org|user]/[project]/_git/[repo]
235+
t.Run("valid user HTTPS remote", func(t *testing.T) {
236+
remoteUrl := "https://user@visualstudio.com/org/project/_git/repo"
237+
expected := &azdoRemote{
238+
Project: "project",
239+
RepositoryName: "repo",
240+
}
241+
242+
result, err := parseAzDoRemote(remoteUrl)
243+
244+
require.NoError(t, err)
245+
require.Equal(t, expected, result)
246+
})
247+
248+
// the url can be in the form of:
249+
// - https://[org].visualstudio.com/[project]/_git/[repo]
250+
t.Run("valid legacy HTTPS remote", func(t *testing.T) {
251+
remoteUrl := "https://visualstudio.com/org/project/_git/repo"
252+
expected := &azdoRemote{
253+
Project: "project",
254+
RepositoryName: "repo",
255+
}
256+
257+
result, err := parseAzDoRemote(remoteUrl)
258+
259+
require.NoError(t, err)
260+
require.Equal(t, expected, result)
261+
})
262+
263+
t.Run("valid legacy HTTPS remote with org", func(t *testing.T) {
264+
remoteUrl := "https://org.visualstudio.com/org/project/_git/repo"
265+
expected := &azdoRemote{
266+
Project: "project",
267+
RepositoryName: "repo",
268+
}
269+
270+
result, err := parseAzDoRemote(remoteUrl)
271+
272+
require.NoError(t, err)
273+
require.Equal(t, expected, result)
274+
})
275+
276+
// the url can be in the form of:
277+
// - git@ssh.dev.azure.com:v[1-3]/[user|org]/[project]/[repo]
278+
// - git@vs-ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
279+
// - git@ssh.visualstudio.com:v[1-3]/[user|org]/[project]/[repo]
280+
t.Run("valid SSH remote", func(t *testing.T) {
281+
remoteUrl := "git@ssh.dev.azure.com:v3/org/project/repo"
282+
expected := &azdoRemote{
283+
Project: "project",
284+
RepositoryName: "repo",
285+
}
286+
287+
result, err := parseAzDoRemote(remoteUrl)
288+
289+
require.NoError(t, err)
290+
require.Equal(t, expected, result)
291+
})
292+
293+
t.Run("valid legacy SSH remote", func(t *testing.T) {
294+
remoteUrl := "git@vs-ssh.visualstudio.com:v3/org/project/repo"
295+
expected := &azdoRemote{
296+
Project: "project",
297+
RepositoryName: "repo",
298+
}
299+
300+
result, err := parseAzDoRemote(remoteUrl)
301+
302+
require.NoError(t, err)
303+
require.Equal(t, expected, result)
304+
})
305+
306+
t.Run("valid legacy SSH remote", func(t *testing.T) {
307+
remoteUrl := "git@ssh.visualstudio.com:v3/org/project/repo"
308+
expected := &azdoRemote{
309+
Project: "project",
310+
RepositoryName: "repo",
311+
}
312+
313+
result, err := parseAzDoRemote(remoteUrl)
314+
315+
require.NoError(t, err)
316+
require.Equal(t, expected, result)
317+
})
318+
319+
t.Run("invalid remote", func(t *testing.T) {
320+
remoteUrl := "https://github.com/user/repo"
321+
322+
result, err := parseAzDoRemote(remoteUrl)
323+
324+
require.Error(t, err)
325+
require.Nil(t, result)
326+
})
327+
}

0 commit comments

Comments
 (0)