Skip to content
This repository was archived by the owner on Sep 20, 2022. It is now read-only.

Commit 62a6d4a

Browse files
committed
Merge pull request #18 from JakeGinnivan/JiraSupport
Added support for Jira fixing #14
2 parents 01e573d + 1230942 commit 62a6d4a

9 files changed

Lines changed: 265 additions & 2 deletions

File tree

src/GitReleaseNotes.Tests/GitReleaseNotes.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<Compile Include="CommitGrouperTests.cs" />
6969
<Compile Include="IssueTrackers\GitHub\GitHubIssueTrackerTests.cs" />
7070
<Compile Include="IssueTrackers\IssueNumberExtractor.cs" />
71+
<Compile Include="IssueTrackers\Jira\JiraIssueTrackerTests.cs" />
7172
<Compile Include="Properties\AssemblyInfo.cs" />
7273
<Compile Include="ReleaseFileWriterTests.cs" />
7374
<Compile Include="ReleaseNotesGeneratorTests.cs" />
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using GitReleaseNotes.IssueTrackers;
4+
using GitReleaseNotes.IssueTrackers.Jira;
5+
using LibGit2Sharp;
6+
using NSubstitute;
7+
using Xunit;
8+
9+
namespace GitReleaseNotes.Tests.IssueTrackers.Jira
10+
{
11+
public class JiraIssueTrackerTests
12+
{
13+
private readonly IJiraApi _jiraApi;
14+
private readonly JiraIssueTracker _sut;
15+
16+
public JiraIssueTrackerTests()
17+
{
18+
_jiraApi = Substitute.For<IJiraApi>();
19+
_sut = new JiraIssueTracker(new IssueNumberExtractor(), _jiraApi);
20+
}
21+
22+
[Fact]
23+
public void CreatesReleaseNotesForClosedGitHubIssues()
24+
{
25+
var commit = CreateCommit("Fixes JIRA-5", DateTimeOffset.Now.AddDays(-1));
26+
var commitsToScan = new List<Commit> { commit };
27+
var toScan = new Dictionary<ReleaseInfo, List<Commit>>
28+
{
29+
{new ReleaseInfo(), commitsToScan}
30+
};
31+
_jiraApi
32+
.GetPotentialIssues(Arg.Any<Dictionary<ReleaseInfo, List<Commit>>>(), Arg.Any<GitReleaseNotesArguments>())
33+
.Returns(new List<JiraIssue>
34+
{
35+
new JiraIssue
36+
{
37+
Id = "JIRA-5",
38+
Name = "Issue Title"
39+
}
40+
});
41+
42+
var releaseNotes = _sut.ScanCommitMessagesForReleaseNotes(new GitReleaseNotesArguments
43+
{
44+
JiraServer = "http://my.jira.net",
45+
JiraProjectId = "JIRA"
46+
}, toScan);
47+
48+
Assert.Equal("Issue Title", releaseNotes.Releases[0].ReleaseNoteItems[0].Title);
49+
}
50+
51+
private static Commit CreateCommit(string message, DateTimeOffset when)
52+
{
53+
var commit = Substitute.For<Commit>();
54+
commit.Message.Returns(message);
55+
var commitSignature = new Signature("Jake", "", when);
56+
commit.Author.Returns(commitSignature);
57+
return commit;
58+
}
59+
}
60+
}

src/GitReleaseNotes/GitReleaseNotes.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<Reference Include="System.ComponentModel.DataAnnotations" />
4848
<Reference Include="System.Core" />
4949
<Reference Include="System.Net.Http" />
50+
<Reference Include="System.Web.Helpers, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
5051
<Reference Include="System.Xml.Linq" />
5152
<Reference Include="System.Data.DataSetExtensions" />
5253
<Reference Include="Microsoft.CSharp" />
@@ -70,6 +71,9 @@
7071
<Compile Include="IssueTrackers\IssueNumberExtractor.cs" />
7172
<Compile Include="IssueTrackers\IIssueTracker.cs" />
7273
<Compile Include="IssueTrackers\IssueTracker.cs" />
74+
<Compile Include="IssueTrackers\Jira\JiraApi.cs" />
75+
<Compile Include="IssueTrackers\Jira\JiraIssue.cs" />
76+
<Compile Include="IssueTrackers\Jira\JiraIssueTracker.cs" />
7377
<Compile Include="Log.cs" />
7478
<Compile Include="Program.cs" />
7579
<Compile Include="Properties\AssemblyInfo.cs" />

src/GitReleaseNotes/GitReleaseNotesArguments.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class GitReleaseNotesArguments
1212
[Description("Enables verbose logging")]
1313
public bool Verbose { get; set; }
1414

15-
[Description("Specifies the issue tracker used, possible Options: GitHub")]
15+
[Description("Specifies the issue tracker used, possible Options: GitHub, Jira")]
1616
public IssueTracker? IssueTracker { get; set; }
1717

1818
[Description("Speficy the tag name to start from, default is the last tag on master")]
@@ -21,6 +21,21 @@ public class GitReleaseNotesArguments
2121
[Description("GitHub access token")]
2222
public string Token { get; set; }
2323

24+
[Description("Jira Username")]
25+
public string Username { get; set; }
26+
27+
[Description("Jira Password")]
28+
public string Password { get; set; }
29+
30+
[Description("Jira project Id")]
31+
public string JiraProjectId { get; set; }
32+
33+
[Description("Jql query for closed issues you would like included if mentioned. Defaults to project = <YOURPROJECTID> AND (issuetype = Bug OR issuetype = Story OR issuetype = \"New Feature\") AND status in (Closed, Done, Resolved)")]
34+
public string Jql { get; set; }
35+
36+
[Description("Url of Jira server")]
37+
public string JiraServer { get; set; }
38+
2439
[Description("GitHub Repository name, in Organisation/Repository format")]
2540
public string Repo { get; set; }
2641

src/GitReleaseNotes/IssueTrackers/IssueTracker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ namespace GitReleaseNotes.IssueTrackers
33
public enum IssueTracker
44
{
55
GitHub,
6+
Jira
67
//TODO Tfs
7-
//TODO Jira
88
//TODO Youtrack
99
}
1010
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Net;
6+
using System.Text;
7+
using System.Web.Helpers;
8+
using LibGit2Sharp;
9+
10+
namespace GitReleaseNotes.IssueTrackers.Jira
11+
{
12+
public interface IJiraApi
13+
{
14+
IEnumerable<JiraIssue> GetPotentialIssues(Dictionary<ReleaseInfo, List<Commit>> releases, GitReleaseNotesArguments arguments);
15+
}
16+
17+
public class JiraApi : IJiraApi
18+
{
19+
public IEnumerable<JiraIssue> GetPotentialIssues(Dictionary<ReleaseInfo, List<Commit>> releases, GitReleaseNotesArguments arguments)
20+
{
21+
var since = releases.SelectMany(c => c.Value).Select(c => c.Author.When).Min();
22+
var sinceFormatted = since.ToString("yyyy-MM-d HH:mm");
23+
var jql = string.Format("{0} AND updated > '{1}'", arguments.Jql, sinceFormatted).Replace("\"", "\\\"");
24+
25+
var searchUri = new Uri(new Uri(arguments.JiraServer, UriKind.Absolute), "/rest/api/latest/search");
26+
var httpRequest = WebRequest.CreateHttp(searchUri);
27+
var usernameAndPass = string.Format("{0}:{1}", arguments.Username, arguments.Password);
28+
var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(usernameAndPass));
29+
httpRequest.Headers.Add("Authorization", string.Format("Basic {0}", token));
30+
httpRequest.Method = "POST";
31+
httpRequest.ContentType = "application/json";
32+
33+
using (var streamWriter = new StreamWriter(httpRequest.GetRequestStream()))
34+
{
35+
string json = "{\"jql\": \"" + jql + "\",\"startAt\": 0, \"maxResults\": 100, \"fields\": [\"summary\",\"issuetype\"]}";
36+
streamWriter.Write(json);
37+
streamWriter.Flush();
38+
streamWriter.Close();
39+
}
40+
41+
var response = (HttpWebResponse)httpRequest.GetResponse();
42+
if ((int)response.StatusCode == 400)
43+
{
44+
throw new Exception("Jql query error, please review your Jql");
45+
}
46+
47+
if (response.StatusCode != HttpStatusCode.OK)
48+
throw new Exception("Failed to query Jira: " + response.StatusDescription);
49+
50+
using (var responseStream = response.GetResponseStream())
51+
using (var responseReader = new StreamReader(responseStream))
52+
{
53+
var responseObject = Json.Decode(responseReader.ReadToEnd());
54+
foreach (var issue in responseObject.issues)
55+
{
56+
string summary = issue.fields.summary;
57+
string id = issue.key;
58+
string issueType = issue.fields.issuetype.name;
59+
60+
yield return new JiraIssue
61+
{
62+
Id = id,
63+
Name = summary,
64+
IssueType = issueType
65+
};
66+
}
67+
}
68+
}
69+
}
70+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace GitReleaseNotes.IssueTrackers.Jira
2+
{
3+
public class JiraIssue
4+
{
5+
public string Id { get; set; }
6+
public string Name { get; set; }
7+
public string IssueType { get; set; }
8+
}
9+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using GitReleaseNotes.IssueTrackers.GitHub;
5+
using LibGit2Sharp;
6+
7+
namespace GitReleaseNotes.IssueTrackers.Jira
8+
{
9+
public class JiraIssueTracker : IIssueTracker
10+
{
11+
private readonly IIssueNumberExtractor _issueNumberExtractor;
12+
private readonly IJiraApi _jiraApi;
13+
14+
public JiraIssueTracker(IIssueNumberExtractor issueNumberExtractor, IJiraApi jiraApi)
15+
{
16+
_issueNumberExtractor = issueNumberExtractor;
17+
_jiraApi = jiraApi;
18+
}
19+
20+
public SemanticReleaseNotes ScanCommitMessagesForReleaseNotes(
21+
GitReleaseNotesArguments arguments,
22+
Dictionary<ReleaseInfo, List<Commit>> releases)
23+
{
24+
var issueNumberRegexPattern = string.Format(@"(?<issueNumber>{0}-\d+)", arguments.JiraProjectId);
25+
var issueNumbersToScan = _issueNumberExtractor.GetIssueNumbers(arguments, releases, issueNumberRegexPattern);
26+
27+
var potentialIssues = _jiraApi.GetPotentialIssues(releases, arguments).ToArray();
28+
29+
var closedMentionedIssuesByRelease = issueNumbersToScan.Select(issues =>
30+
{
31+
var issuesForRelease = potentialIssues
32+
.Where(i => issues.Value.Contains(i.Id))
33+
.ToArray();
34+
return new
35+
{
36+
ReleaseInfo = issues.Key,
37+
IssuesForRelease = issuesForRelease
38+
};
39+
})
40+
.Where(g => g.IssuesForRelease.Any())
41+
.OrderBy(g => g.ReleaseInfo.When);
42+
43+
var baseUrl = new Uri(arguments.JiraServer, UriKind.Absolute);
44+
return new SemanticReleaseNotes(closedMentionedIssuesByRelease.Select(r =>
45+
{
46+
var releaseNoteItems = r.IssuesForRelease.Select(i =>
47+
{
48+
var htmlUrl = new Uri(baseUrl, string.Format("browse/{0}", i.Id));
49+
return new ReleaseNoteItem(i.Name, i.Id, htmlUrl, new[] { i.IssueType });
50+
}).ToArray();
51+
return new SemanticRelease(r.ReleaseInfo.Name, r.ReleaseInfo.When, releaseNoteItems);
52+
}));
53+
}
54+
55+
public bool VerifyArgumentsAndWriteErrorsToConsole(GitReleaseNotesArguments arguments)
56+
{
57+
if (string.IsNullOrEmpty(arguments.JiraServer) ||
58+
!Uri.IsWellFormedUriString(arguments.JiraServer, UriKind.Absolute))
59+
{
60+
Console.WriteLine("A valid Jira server must be specified [/JiraServer ]");
61+
return false;
62+
}
63+
64+
if (string.IsNullOrEmpty(arguments.JiraProjectId))
65+
{
66+
Console.WriteLine("/JiraProjectId is a required parameter for Jira");
67+
return false;
68+
}
69+
70+
if (string.IsNullOrEmpty(arguments.Username))
71+
{
72+
Console.WriteLine("/Username is a required to authenticate with Jira");
73+
return false;
74+
}
75+
if (string.IsNullOrEmpty(arguments.Password))
76+
{
77+
Console.WriteLine("/Password is a required to authenticate with Jira");
78+
return false;
79+
}
80+
81+
if (string.IsNullOrEmpty(arguments.Jql))
82+
{
83+
arguments.Jql = string.Format("project = {0} AND " +
84+
"(issuetype = Bug OR issuetype = Story OR issuetype = \"New Feature\") AND " +
85+
"status in (Closed, Done, Resolved)", arguments.JiraProjectId);
86+
}
87+
88+
return true;
89+
}
90+
91+
public void PublishRelease(string releaseNotesOutput, GitReleaseNotesArguments arguments)
92+
{
93+
Console.WriteLine("Jira does not support publishing releases");
94+
}
95+
}
96+
}

src/GitReleaseNotes/Program.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using GitReleaseNotes.Git;
1010
using GitReleaseNotes.IssueTrackers;
1111
using GitReleaseNotes.IssueTrackers.GitHub;
12+
using GitReleaseNotes.IssueTrackers.Jira;
1213
using Octokit;
1314
using Credentials = Octokit.Credentials;
1415
using Repository = LibGit2Sharp.Repository;
@@ -38,6 +39,9 @@ static int Main(string[] args)
3839
return 1;
3940

4041
CreateIssueTrackers(arguments);
42+
if (!IssueTrackers.ContainsKey(arguments.IssueTracker.Value))
43+
throw new Exception(string.Format("{0} is not a known issue tracker", arguments.IssueTracker.Value));
44+
4145
var issueTracker = IssueTrackers[arguments.IssueTracker.Value];
4246
if (!issueTracker.VerifyArgumentsAndWriteErrorsToConsole(arguments))
4347
return 1;
@@ -109,6 +113,10 @@ private static void CreateIssueTrackers(GitReleaseNotesArguments arguments)
109113
{
110114
Credentials = new Credentials(arguments.Token)
111115
}, new Log())
116+
},
117+
{
118+
IssueTracker.Jira,
119+
new JiraIssueTracker(new IssueNumberExtractor(), new JiraApi())
112120
}
113121
};
114122
}

0 commit comments

Comments
 (0)