Skip to content

Commit 7fcf0ee

Browse files
CopilotMrHinsh
andcommitted
Support actual new Azure DevOps Git link format with forward slashes
The new format uses forward slashes instead of %2f encoding: - Legacy: vstfs:///Git/Commit/{projectId}%2f{repoId}%2f{commitId} - New: vstfs:///Git/Commit/{projectName}/{repoName}/{commitId} Key differences: - New format uses / separator instead of %2f - New format uses project/repo names instead of GUIDs - Repo lookup is by name for new format, by ID for legacy Updated tests to cover both formats correctly Co-authored-by: MrHinsh <5205575+MrHinsh@users.noreply.github.com>
1 parent e21a3a7 commit 7fcf0ee

File tree

2 files changed

+100
-31
lines changed

2 files changed

+100
-31
lines changed

src/MigrationTools.Clients.TfsObjectModel.Tests/Tools/TfsGitRepositoryInfoTests.cs

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ private ExternalLink CreateMockExternalLink(string uri)
3030
}
3131

3232
[TestMethod(), TestCategory("L0")]
33-
public void CreateFromGit_ValidLinkWithThreeParts_ShouldSucceed()
33+
public void CreateFromGit_ValidLinkWithThreeParts_LegacyFormat_ShouldSucceed()
3434
{
35-
// Arrange
35+
// Arrange - Legacy format with %2f encoding
3636
var validLink = "vstfs:///Git/Commit/25f94570-e3e7-4b79-ad19-4b434787fd5a%2f50477259-3058-4dff-ba4c-e8c179ec5327%2f41dd2754058348d72a6417c0615c2543b9b55535";
3737
var externalLink = CreateMockExternalLink(validLink);
3838
var possibleRepos = new List<GitRepository>
@@ -50,9 +50,9 @@ public void CreateFromGit_ValidLinkWithThreeParts_ShouldSucceed()
5050
}
5151

5252
[TestMethod(), TestCategory("L0")]
53-
public void CreateFromGit_ValidLinkWithMultipleCommitParts_ShouldSucceed()
53+
public void CreateFromGit_ValidLinkWithMultipleCommitParts_LegacyFormat_ShouldSucceed()
5454
{
55-
// Arrange
55+
// Arrange - Legacy format with extended commit ID
5656
var validLink = "vstfs:///Git/Commit/25f94570-e3e7-4b79-ad19-4b434787fd5a%2f50477259-3058-4dff-ba4c-e8c179ec5327%2f41dd2754058348d72a6417c0615c2543b9b55535%2fextra%2fparts";
5757
var externalLink = CreateMockExternalLink(validLink);
5858
var possibleRepos = new List<GitRepository>
@@ -70,14 +70,18 @@ public void CreateFromGit_ValidLinkWithMultipleCommitParts_ShouldSucceed()
7070
}
7171

7272
[TestMethod(), TestCategory("L0")]
73-
public void CreateFromGit_ValidLinkWithTwoParts_NewFormat_ShouldSucceed()
73+
public void CreateFromGit_ValidLinkWithSlashes_NewFormat_ShouldSucceed()
7474
{
75-
// Arrange - New format without projectId
76-
var newFormatLink = "vstfs:///Git/Commit/50477259-3058-4dff-ba4c-e8c179ec5327%2f41dd2754058348d72a6417c0615c2543b9b55535";
75+
// Arrange - New Azure DevOps format with forward slashes
76+
var newFormatLink = "vstfs:///Git/Commit/MyProject/MyRepo/41dd2754058348d72a6417c0615c2543b9b55535";
7777
var externalLink = CreateMockExternalLink(newFormatLink);
7878
var possibleRepos = new List<GitRepository>
7979
{
80-
new GitRepository { Id = Guid.Parse("50477259-3058-4dff-ba4c-e8c179ec5327") }
80+
new GitRepository
81+
{
82+
Id = Guid.Parse("50477259-3058-4dff-ba4c-e8c179ec5327"),
83+
Name = "MyRepo"
84+
}
8185
};
8286

8387
// Act
@@ -87,12 +91,14 @@ public void CreateFromGit_ValidLinkWithTwoParts_NewFormat_ShouldSucceed()
8791
Assert.IsNotNull(result);
8892
Assert.AreEqual("50477259-3058-4dff-ba4c-e8c179ec5327", result.RepoID);
8993
Assert.AreEqual("41dd2754058348d72a6417c0615c2543b9b55535", result.CommitID);
94+
Assert.IsNotNull(result.GitRepo);
95+
Assert.AreEqual("MyRepo", result.GitRepo.Name);
9096
}
9197

9298
[TestMethod(), TestCategory("L0")]
93-
public void CreateFromGit_InvalidLinkWithOnePart_ShouldReturnNull()
99+
public void CreateFromGit_InvalidLinkWithOnePart_LegacyFormat_ShouldReturnNull()
94100
{
95-
// Arrange
101+
// Arrange - Legacy format with insufficient parts
96102
var invalidLink = "vstfs:///Git/Commit/25f94570-e3e7-4b79-ad19-4b434787fd5a";
97103
var externalLink = CreateMockExternalLink(invalidLink);
98104
var possibleRepos = new List<GitRepository>();
@@ -104,6 +110,36 @@ public void CreateFromGit_InvalidLinkWithOnePart_ShouldReturnNull()
104110
Assert.IsNull(result);
105111
}
106112

113+
[TestMethod(), TestCategory("L0")]
114+
public void CreateFromGit_InvalidLinkWithTwoParts_LegacyFormat_ShouldReturnNull()
115+
{
116+
// Arrange - Legacy format with only 2 parts (missing commitId)
117+
var invalidLink = "vstfs:///Git/Commit/25f94570-e3e7-4b79-ad19-4b434787fd5a%2f50477259-3058-4dff-ba4c-e8c179ec5327";
118+
var externalLink = CreateMockExternalLink(invalidLink);
119+
var possibleRepos = new List<GitRepository>();
120+
121+
// Act
122+
var result = TfsGitRepositoryInfo.CreateFromGit(externalLink, possibleRepos);
123+
124+
// Assert
125+
Assert.IsNull(result);
126+
}
127+
128+
[TestMethod(), TestCategory("L0")]
129+
public void CreateFromGit_InvalidLinkWithTwoParts_NewFormat_ShouldReturnNull()
130+
{
131+
// Arrange - New format with insufficient parts
132+
var invalidLink = "vstfs:///Git/Commit/MyProject/MyRepo";
133+
var externalLink = CreateMockExternalLink(invalidLink);
134+
var possibleRepos = new List<GitRepository>();
135+
136+
// Act
137+
var result = TfsGitRepositoryInfo.CreateFromGit(externalLink, possibleRepos);
138+
139+
// Assert
140+
Assert.IsNull(result);
141+
}
142+
107143
[TestMethod(), TestCategory("L0")]
108144
public void CreateFromGit_EmptyLink_ShouldReturnNull()
109145
{

src/MigrationTools.Clients.TfsObjectModel/Tools/TfsGitRepositoryInfo.cs

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -91,40 +91,73 @@ public static TfsGitRepositoryInfo CreateFromGit(ExternalLink gitExternalLink, I
9191
string commitID;
9292
string repoID;
9393
GitRepository gitRepo;
94-
//Old format: vstfs:///Git/Commit/25f94570-e3e7-4b79-ad19-4b434787fd5a%2f50477259-3058-4dff-ba4c-e8c179ec5327%2f41dd2754058348d72a6417c0615c2543b9b55535
95-
//New format: vstfs:///Git/Commit/50477259-3058-4dff-ba4c-e8c179ec5327%2f41dd2754058348d72a6417c0615c2543b9b55535
96-
string guidbits = gitExternalLink.LinkedArtifactUri.Substring(gitExternalLink.LinkedArtifactUri.LastIndexOf('/') + 1);
97-
string[] bits = Regex.Split(guidbits, "%2f", RegexOptions.IgnoreCase);
94+
//Legacy format: vstfs:///Git/Commit/25f94570-e3e7-4b79-ad19-4b434787fd5a%2f50477259-3058-4dff-ba4c-e8c179ec5327%2f41dd2754058348d72a6417c0615c2543b9b55535
95+
//New format: vstfs:///Git/Commit/projectName/repoName/commitId
9896

99-
// Validate that we have at least 2 parts (repoId, commitId) for new format
100-
// or 3 parts (projectId, repoId, commitId) for old format
101-
if (bits.Length < 2)
97+
// Determine which format we're dealing with
98+
if (gitExternalLink.LinkedArtifactUri.Contains("%2f", StringComparison.OrdinalIgnoreCase))
10299
{
103-
Log.Warning("GitRepositoryInfo: Invalid Git external link format. Expected at least 2 parts separated by %2f, but got {count} parts. Link: {link}", bits.Length, gitExternalLink.LinkedArtifactUri);
104-
return null;
105-
}
106-
107-
// Support both old format (3+ parts) and new format (2 parts)
108-
if (bits.Length >= 3)
109-
{
110-
// Old format: projectId%2frepoId%2fcommitId
100+
// Legacy format with %2f encoding
101+
string guidbits = gitExternalLink.LinkedArtifactUri.Substring(gitExternalLink.LinkedArtifactUri.LastIndexOf('/') + 1);
102+
string[] bits = Regex.Split(guidbits, "%2f", RegexOptions.IgnoreCase);
103+
104+
// Validate that we have at least 3 parts (projectId, repoId, commitId) for legacy format
105+
if (bits.Length < 3)
106+
{
107+
Log.Warning("GitRepositoryInfo: Invalid Git external link format (legacy). Expected at least 3 parts separated by %2f, but got {count} parts. Link: {link}", bits.Length, gitExternalLink.LinkedArtifactUri);
108+
return null;
109+
}
110+
111+
// Legacy format: projectId%2frepoId%2fcommitId
111112
repoID = bits[1];
112113
commitID = $"{bits[2]}";
113114
for (int i = 3; i < bits.Count(); i++)
114115
{
115116
commitID += $"%2f{bits[i]}";
116117
}
118+
119+
gitRepo = (from g in possibleRepos
120+
where string.Equals(g.Id.ToString(), repoID, StringComparison.OrdinalIgnoreCase)
121+
select g).SingleOrDefault();
117122
}
118123
else
119124
{
120-
// New format: repoId%2fcommitId
121-
repoID = bits[0];
122-
commitID = bits[1];
125+
// New format with forward slashes: vstfs:///Git/Commit/projectName/repoName/commitId
126+
const string prefix = "vstfs:///Git/Commit/";
127+
if (!gitExternalLink.LinkedArtifactUri.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
128+
{
129+
Log.Warning("GitRepositoryInfo: Invalid Git external link format (new). Link does not start with expected prefix. Link: {link}", gitExternalLink.LinkedArtifactUri);
130+
return null;
131+
}
132+
133+
string remainder = gitExternalLink.LinkedArtifactUri.Substring(prefix.Length);
134+
string[] parts = remainder.Split('/');
135+
136+
// Validate that we have at least 3 parts (projectName, repoName, commitId)
137+
if (parts.Length < 3)
138+
{
139+
Log.Warning("GitRepositoryInfo: Invalid Git external link format (new). Expected at least 3 parts separated by /, but got {count} parts. Link: {link}", parts.Length, gitExternalLink.LinkedArtifactUri);
140+
return null;
141+
}
142+
143+
// New format: projectName/repoName/commitId
144+
string repoName = parts[1];
145+
commitID = parts[2];
146+
147+
// Handle commit IDs that may contain additional slashes
148+
for (int i = 3; i < parts.Length; i++)
149+
{
150+
commitID += $"/{parts[i]}";
151+
}
152+
153+
// Look up repo by name instead of ID
154+
gitRepo = (from g in possibleRepos
155+
where string.Equals(g.Name, repoName, StringComparison.OrdinalIgnoreCase)
156+
select g).SingleOrDefault();
157+
158+
repoID = gitRepo?.Id.ToString();
123159
}
124160

125-
gitRepo =
126-
(from g in possibleRepos where string.Equals(g.Id.ToString(), repoID, StringComparison.OrdinalIgnoreCase) select g)
127-
.SingleOrDefault();
128161
return new TfsGitRepositoryInfo(commitID, repoID, gitRepo);
129162
}
130163

0 commit comments

Comments
 (0)