public static synchronized GitHubService getGitHubService(
    String api, final Optional<String> token) {
  if (!api.endsWith("/")) {
    api += "/";
  }
  final File cacheDir = new File(".okhttpcache");
  cacheDir.mkdir();
  final Cache cache = new Cache(cacheDir, 1024 * 1024 * 10);

  final OkHttpClient.Builder builder =
      new OkHttpClient.Builder().cache(cache).connectTimeout(10, SECONDS);

  if (token != null && token.isPresent() && !token.get().isEmpty()) {
    builder.addInterceptor(
        chain -> {
          final Request original = chain.request();

          final Request request =
              original
                  .newBuilder() //
                  .addHeader("Authorization", "token " + token.get()) //
                  .method(original.method(), original.body()) //
                  .build();
          return chain.proceed(request);
        });
  }

  if (interceptor != null) {
    builder.addInterceptor(interceptor);
  }

  final Retrofit retrofit =
      new Retrofit.Builder() //
          .baseUrl(api) //
          .client(builder.build()) //
          .addConverterFactory(GsonConverterFactory.create()) //
          .build();

  return retrofit.create(GitHubService.class);
} public class GitChangelogIntegrationException extends Exception {

  private static final long serialVersionUID = 4249741847365803709L;

  public GitChangelogIntegrationException(String message, Throwable throwable) {
    super(message, throwable);
  }

  public GitChangelogIntegrationException(String message) {
    super(message);
  }
} public class GitCommit implements Comparable<GitCommit> {
  private final String authorEmailAddress;
  private final String authorName;
  private final Date commitTime;
  private final String hash;
  private final Boolean merge;
  private final String message;

  public GitCommit(
      String authorName,
      String authorEmailAddress,
      Date commitTime,
      String message,
      String hash,
      Boolean merge) {
    this.authorEmailAddress = authorEmailAddress;
    this.authorName = authorName;
    this.commitTime = commitTime;
    this.message = message;
    this.hash = hash;
    this.merge = merge;
  }

  @Override
  public int compareTo(GitCommit o) {
    int compareTo = o.commitTime.compareTo(this.commitTime);
    if (compareTo == 0) {
      return o.hash.compareTo(this.hash);
    }
    return compareTo;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    GitCommit other = (GitCommit) obj;
    if (this.authorEmailAddress == null) {
      if (other.authorEmailAddress != null) {
        return false;
      }
    } else if (!this.authorEmailAddress.equals(other.authorEmailAddress)) {
      return false;
    }
    if (this.authorName == null) {
      if (other.authorName != null) {
        return false;
      }
    } else if (!this.authorName.equals(other.authorName)) {
      return false;
    }
    if (this.commitTime == null) {
      if (other.commitTime != null) {
        return false;
      }
    } else if (!this.commitTime.equals(other.commitTime)) {
      return false;
    }
    if (this.hash == null) {
      if (other.hash != null) {
        return false;
      }
    } else if (!this.hash.equals(other.hash)) {
      return false;
    }
    if (this.merge == null) {
      if (other.merge != null) {
        return false;
      }
    } else if (!this.merge.equals(other.merge)) {
      return false;
    }
    if (this.message == null) {
      if (other.message != null) {
        return false;
      }
    } else if (!this.message.equals(other.message)) {
      return false;
    }
    return true;
  }

  public String getAuthorEmailAddress() {
    return this.authorEmailAddress;
  }

  public String getAuthorName() {
    return this.authorName;
  }

  public Date getCommitTime() {
    return this.commitTime;
  }

  public String getHash() {
    return this.hash;
  }

  public String getMessage() {
    return this.message;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result =
        prime * result
            + ((this.authorEmailAddress == null) ? 0 : this.authorEmailAddress.hashCode());
    result = prime * result + ((this.authorName == null) ? 0 : this.authorName.hashCode());
    result = prime * result + ((this.commitTime == null) ? 0 : this.commitTime.hashCode());
    result = prime * result + ((this.hash == null) ? 0 : this.hash.hashCode());
    result = prime * result + ((this.merge == null) ? 0 : this.merge.hashCode());
    result = prime * result + ((this.message == null) ? 0 : this.message.hashCode());
    return result;
  }

  public Boolean isMerge() {
    return this.merge;
  }

  @Override
  public String toString() {
    return "GitCommit [authorEmailAddress="
        + this.authorEmailAddress
        + ", authorName="
        + this.authorName
        + ", commitTime="
        + this.commitTime
        + ", hash="
        + this.hash
        + ", merge="
        + this.merge
        + ", message="
        + this.message
        + "]";
  }
} public class GitHubHelper {

  private static Pattern PAGE_PATTERN = Pattern.compile("page=([0-9]+)>");
  private final GitHubService service;

  public GitHubHelper(final GitHubService service) {
    this.service = service;
  }

  public Optional<GitHubIssue> getIssueFromAll(String issue)
      throws GitChangelogIntegrationException {
    if (issue.startsWith("#")) {
      issue = issue.substring(1);
    }

    int page = 1;
    while (page > 0) {
      final Call<List<GitHubIssue>> call = this.service.issues(page);
      page = -1;

      try {
        final Response<List<GitHubIssue>> response = call.execute();

        if (!response.isSuccessful()) {
          throw new GitChangelogIntegrationException(
              "Request:"
                  + response.raw().request().toString()
                  + "\nError:\n"
                  + response.errorBody().string());
        }

        // Pagination
        if (response.headers().get("Link") != null) {
          final String link = response.headers().get("Link");
          String parsedPage = null;
          PART:
          for (final String part : Arrays.asList(link.split(","))) {
            for (final String piece : Arrays.asList(part.split(";"))) {
              if ("rel=\"next\"".equals(piece.trim()) && parsedPage != null) {
                // Previous piece pointed to next
                page = Integer.parseInt(parsedPage);
                break PART;
              } else if (piece.contains("&page=")) {
                final Matcher match = PAGE_PATTERN.matcher(piece);
                if (match.find()) {
                  parsedPage = match.group(1);
                }
              }
            }
          }
        }

        for (final GitHubIssue gitHubIssue : response.body()) {
          if (issue.equals(gitHubIssue.getNumber())) {
            return of(gitHubIssue);
          }
        }

      } catch (final IOException e) {
        throw new GitChangelogIntegrationException(issue, e);
      }
    }
    return Optional.empty();
  }
} public class GitHubIssue {
  public final String title;
  public final String html_url;
  public final String number;
  private final List<GitHubLabel> labels;

  public GitHubIssue(String title, String link, String number, List<GitHubLabel> labels) {
    this.title = title;
    this.html_url = link;
    this.number = number;
    this.labels = labels;
  }

  public List<GitHubLabel> getLabels() {
    return labels;
  }

  public String getNumber() {
    return number;
  }

  public String getLink() {
    return html_url;
  }

  public String getTitle() {
    return title;
  }

  @Override
  public String toString() {
    return "GitHubIssue [title="
        + title
        + ", html_url="
        + html_url
        + ", number="
        + number
        + ", labels="
        + labels
        + "]";
  }
} public class GitHubLabel {
  private final String name;

  public GitHubLabel(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  @Override
  public String toString() {
    return "GitHubLabel [name=" + name + "]";
  }
} public class GitLabClient {

  private final String hostUrl;
  private final String apiToken;

  private List<GitlabIssue> issues;

  public GitLabClient(final String hostUrl, final String apiToken) {
    this.hostUrl = hostUrl;
    this.apiToken = apiToken;
  }

  public Optional<GitLabIssue> getIssue(final String projectName, final Integer matchedIssue)
      throws GitChangelogIntegrationException {
    final GitlabAPI gitLabApi = GitlabAPI.connect(this.hostUrl, this.apiToken);
    GitlabProject project;
    try {
      project = gitLabApi.getProject(projectName);
    } catch (final Exception e) {
      throw new GitChangelogIntegrationException(
          "Unable to find project \""
              + projectName
              + "\". It should be \"tomas.bjerre85/violations-test\" for a repo like: https://gitlab.com/tomas.bjerre85/violations-test",
          e);
    }
    final Integer projectId = project.getId();
    final String httpUrl = project.getHttpUrl();
    try {
      if (this.issues == null) {
        this.issues = this.getAllIssues(this.hostUrl, this.apiToken, projectId);
      }
      for (final GitlabIssue candidate : this.issues) {
        if (candidate.getIid() == matchedIssue) {
          return Optional.of(this.createGitLabIssue(httpUrl, candidate));
        }
      }
      return Optional.empty();
    } catch (final Exception e) {
      throw new GitChangelogIntegrationException(e.getMessage(), e);
    }
  }

  private GitLabIssue createGitLabIssue(final String projectUrl, final GitlabIssue candidate) {
    final String title = candidate.getTitle();
    final String link = projectUrl + "/issues/" + candidate.getIid();
    final List<String> labels = new ArrayList<>();
    for (final String l : candidate.getLabels()) {
      labels.add(l);
    }
    return new GitLabIssue(title, link, labels);
  }

  private List<GitlabIssue> getAllIssues(
      final String hostUrl, final String apiToken, final Integer projectId) throws IOException {
    final GitlabAPI gitLabApi = GitlabAPI.connect(hostUrl, apiToken);
    final GitlabProject project = new GitlabProject();
    project.setId(projectId);
    return gitLabApi.getIssues(project);
  }
} public class GitLabIssue {

  private final String title;
  private final String link;
  private final List<String> labels;

  public GitLabIssue(String title, String link, List<String> labels) {
    this.title = title;
    this.link = link;
    this.labels = labels;
  }

  public List<String> getLabels() {
    return labels;
  }

  public String getLink() {
    return link;
  }

  public String getTitle() {
    return title;
  }

  @Override
  public String toString() {
    return "GitLabIssue [title=" + title + ", link=" + link + ", labels=" + labels + "]";
  }
} public abstract class JiraClient {

  private final String api;

  public JiraClient(final String api) {
    if (api.endsWith("/")) {
      this.api = api.substring(0, api.length() - 1);
    } else {
      this.api = api;
    }
  }

  public String getApi() {
    return this.api;
  }

  protected String getEndpoint(final String issue) {
    final String endpoint =
        this.api
            + "/rest/api/2/issue/"
            + issue
            + "?fields=parent,summary,issuetype,labels,description,issuelinks";
    return endpoint;
  }

  protected JiraIssue toJiraIssue(final String issue, final String json) {
    final String title = read(json, "$.fields.summary");
    final String description = read(json, "$.fields.description");
    final String type = read(json, "$.fields.issuetype.name");
    final String link = this.api + "/browse/" + issue;
    final List<String> labels = JsonPath.read(json, "$.fields.labels");
    final List<String> linkedIssues = new ArrayList<>();
    final List<String> inwardKey = JsonPath.read(json, "$.fields.issuelinks[*].inwardIssue.key");
    final List<String> outwardKey = JsonPath.read(json, "$.fields.issuelinks[*].outwardIssue.key");
    linkedIssues.addAll(inwardKey);
    linkedIssues.addAll(outwardKey);

    final JiraIssue jiraIssue =
        new JiraIssue(title, description, link, issue, type, linkedIssues, labels);
    return jiraIssue;
  }

  public abstract JiraClient withBasicCredentials(String username, String password);

  public abstract JiraClient withBearer(String bearerToken);

  public abstract JiraClient withTokenCredentials(String token);

  public abstract JiraClient withHeaders(Map<String, String> headers);

  public abstract Optional<JiraIssue> getIssue(String matched)
      throws GitChangelogIntegrationException;
} public class JiraClientFactory {

  private static JiraClient jiraClient;

  public static void reset() {
    jiraClient = null;
  }

  /** The Bitbucket Server plugin uses this method to inject the Atlassian Client. */
  public static void setJiraClient(final JiraClient jiraClient) {
    JiraClientFactory.jiraClient = jiraClient;
  }

  public static JiraClient createJiraClient(final String apiUrl) {
    if (jiraClient != null) {
      return jiraClient;
    }
    return new DefaultJiraClient(apiUrl);
  }
} public class JiraIssue {

  private final String title;
  private final String link;
  private final String issue;
  private final String issueType;
  private final String description;
  private final List<String> linkedIssues;
  private final List<String> labels;

  public JiraIssue(
      String title,
      String description,
      String link,
      String issue,
      String issueType,
      List<String> linkedIssues,
      List<String> labels) {
    this.title = title;
    this.link = link;
    this.issue = issue;
    this.issueType = issueType;
    this.linkedIssues = linkedIssues;
    this.labels = labels;
    this.description = description;
  }

  public String getIssue() {
    return issue;
  }

  public String getLink() {
    return link;
  }

  public String getTitle() {
    return title;
  }

  public String getIssueType() {
    return issueType;
  }

  public List<String> getLinkedIssues() {
    return linkedIssues;
  }

  public List<String> getLabels() {
    return labels;
  }

  public String getDescription() {

    return description;
  }

  @Override
  public String toString() {
    return "JiraIssue [title="
        + title
        + ", link="
        + link
        + ", issue="
        + issue
        + ", issueType="
        + issueType
        + ", linkedIssues="
        + linkedIssues
        + ", labels="
        + labels
        + "]";
  }
} public abstract class RedmineClient {
  private final String api;

  public RedmineClient(final String api) {
    if (api.endsWith("/")) {
      this.api = api.substring(0, api.length() - 1);
    } else {
      this.api = api;
    }
  }

  public String getApi() {
    return this.api;
  }

  protected String getEndpoint(final String issue) {
    final String issueNo = getIssueNumber(issue);
    final String endpoint = this.api + "/issues/" + issueNo + ".json";
    return endpoint;
  }

  protected RedmineIssue toRedmineIssue(final String issue, final String json) {
    final String issueNo = getIssueNumber(issue);
    final String title = read(json, "$.issue.subject");
    final String description = read(json, "$.issue.description");
    final String type = read(json, "$.issue.tracker.name");
    final String link = this.api + "/issues/" + issueNo;

    final RedmineIssue redmineIssue = new RedmineIssue(title, description, link, issue, type);
    return redmineIssue;
  }

  protected String getIssueNumber(String issue) {
    return issue.startsWith("#") ? issue.substring(1) : issue;
  }

  public abstract RedmineClient withBasicCredentials(String username, String password);

  public abstract RedmineClient withTokenCredentials(String token);

  public abstract RedmineClient withHeaders(Map<String, String> headers);

  public abstract Optional<RedmineIssue> getIssue(String matched)
      throws GitChangelogIntegrationException;
} public class RedmineClientFactory {

  private static RedmineClient redmineClient;

  public static void reset() {
    redmineClient = null;
  }

  /** The Bitbucket Server plugin uses this method to inject the Atlassian Client. */
  public static void setRedmineClient(final RedmineClient redmineClient) {
    RedmineClientFactory.redmineClient = redmineClient;
  }

  public static RedmineClient createRedmineClient(final String apiUrl) {
    if (redmineClient != null) {
      return redmineClient;
    }
    return new DefaultRedmineClient(apiUrl);
  }
} public class RedmineIssue {
  private final String title;
  private final String link;
  private final String issue;
  private final String issueType;
  private final String description;

  public RedmineIssue(
      String title, String description, String link, String issue, String issueType) {
    this.title = title;
    this.link = link;
    this.issue = issue;
    this.issueType = issueType;
    this.description = description;
  }

  public String getIssue() {
    return issue;
  }

  public String getLink() {
    return link;
  }

  public String getTitle() {
    return title;
  }

  public String getIssueType() {
    return issueType;
  }

  public String getDescription() {

    return description;
  }

  @Override
  public String toString() {
    return "RedmineIssue [title="
        + title
        + ", link="
        + link
        + ", issue="
        + issue
        + ", issueType="
        + issueType
        + "]";
  }
} public class ParsedIssue implements IGitCommitReferer {
  private final List<GitCommit> gitCommits = new ArrayList<>();
  private final String name;
  private final String title;
  private final String link;
  private final String issue;
  private final String issueType;
  private final List<String> linkedIssues;
  private final List<String> labels;
  private final String description;
  private final SettingsIssueType settingsIssueType;

  public ParsedIssue(
      final SettingsIssueType settingsIssueType,
      final String name,
      final String issue,
      final String description,
      final String link,
      final String title,
      final String issueType,
      final List<String> linkedIssues,
      final List<String> labels) {
    this.name = checkNotNull(name, "name");
    this.title = emptyToNull(title);
    this.issue = issue;
    this.link = link;
    this.issueType = issueType;
    this.settingsIssueType = settingsIssueType;
    this.linkedIssues = linkedIssues;
    this.labels = labels;
    this.description = description;
  }

  public SettingsIssueType getSettingsIssueType() {
    return this.settingsIssueType;
  }

  public Optional<String> getTitle() {
    return Optional.ofNullable(this.title);
  }

  @Override
  public GitCommit getGitCommit() {
    return checkNotNull(this.gitCommits.get(0), this.name);
  }

  public List<GitCommit> getGitCommits() {
    return this.gitCommits;
  }

  public String getLink() {
    return this.link;
  }

  public String getIssueType() {
    return this.issueType;
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override
  public int hashCode() {
    return this.toString().hashCode();
  }

  @Override
  public String toString() {
    return this.name + this.issue;
  }

  @Override
  public boolean equals(final Object obj) {
    if (obj == null) {
      return false;
    }
    if (obj.getClass() != ParsedIssue.class) {
      return false;
    }
    return this.name.equals(((ParsedIssue) obj).getName());
  }

  public void addCommit(final GitCommit gitCommit) {
    this.gitCommits.add(gitCommit);
  }

  public String getIssue() {
    return this.issue;
  }

  public String getDescription() {
    return this.description;
  }

  public void addCommits(final List<GitCommit> commits) {
    this.gitCommits.addAll(commits);
  }

  public List<String> getLabels() {
    return this.labels;
  }

  public List<String> getLinkedIssues() {
    return this.linkedIssues;
  }
} public class IssuesUtil {
  private final Settings settings;

  public IssuesUtil(final Settings settings) {
    this.settings = settings;
  }

  public List<SettingsIssue> getIssues() {
    final List<SettingsIssue> issues = new ArrayList<>(this.settings.getCustomIssues());
    if (this.settings.isJiraEnabled()) {
      this.addJira(issues);
    }
    if (this.settings.isGitHubEnabled()) {
      this.addGitHub(issues);
    }
    if (this.settings.isGitLabEnabled()) {
      this.addGitLab(issues);
    }
    if (this.settings.isRedmineEnabled()) {
      this.addRedmine(issues);
    }
    return issues;
  }

  private void addGitHub(final List<SettingsIssue> issues) {
    if (!isNullOrEmpty(this.settings.getGitHubIssuePattern())) {
      if (this.settings.getGitHubApi().isPresent()) {
        issues.add(
            new SettingsIssue(GITHUB, "GitHub", this.settings.getGitHubIssuePattern(), null, null));
      }
    }
  }

  private void addGitLab(final List<SettingsIssue> issues) {
    if (!isNullOrEmpty(this.settings.getGitLabIssuePattern())) {
      if (this.settings.getGitLabServer().isPresent()) {
        issues.add(
            new SettingsIssue(GITLAB, "GitLab", this.settings.getGitLabIssuePattern(), null, null));
      }
    }
  }

  private void addJira(final List<SettingsIssue> issues) {
    if (!isNullOrEmpty(this.settings.getJiraIssuePattern())) {
      if (this.settings.getJiraServer().isPresent()) {
        issues.add(
            new SettingsIssue(
                JIRA,
                "Jira",
                this.settings.getJiraIssuePattern(),
                this.settings.getJiraServer().orElse("") + "/browse/${PATTERN_GROUP}",
                null));
      } else {
        issues.add(
            new SettingsIssue(
                JIRA,
                "Jira",
                this.settings.getJiraIssuePattern(),
                this.settings.getJiraServer().orElse(null),
                null));
      }
    }
  }

  private void addRedmine(final List<SettingsIssue> issues) {
    if (!isNullOrEmpty(this.settings.getRedmineIssuePattern())) {
      if (this.settings.getRedmineServer().isPresent()) {
        issues.add(
            new SettingsIssue(
                REDMINE,
                "Redmine",
                this.settings.getRedmineIssuePattern(),
                this.settings.getRedmineServer().orElse("") + "/issues/${PATTERN_GROUP}",
                null));
      } else {
        issues.add(
            new SettingsIssue(
                REDMINE,
                "Redmine",
                this.settings.getRedmineIssuePattern(),
                this.settings.getRedmineServer().orElse(null),
                null));
      }
    }
  }
} public class Settings implements Serializable {
  private static final long serialVersionUID = 4565886594381385244L;

  private static Gson gson = new Gson();

  /** Folder where repo lives. */
  private String fromRepo;
  /** Include all commits from here. Any tag or branch name. */
  private String fromRef;
  /**
   * Include all commits to this reference. Any tag or branch name. There is a constant for master
   * here: reference{GitChangelogApiConstants#REF_MASTER}.
   */
  private String toRef;
  /**
   * Include all commits from here. Any commit hash. There is a constant pointing at the first
   * commit here: reference{GitChangelogApiConstants#ZERO_COMMIT}.
   */
  private String fromCommit;
  /** Include all commits to here. Any commit hash. */
  private String toCommit;
  /**
   * A regular expression that is evaluated on each tag. If it matches, the tag will be filtered out
   * and not included in the changelog.
   */
  private String ignoreTagsIfNameMatches;
  /**
   * A regular expression that is evaluated on the commit message of each commit. If it matches, the
   * commit will be filtered out and not included in the changelog.<br>
   * <br>
   * To ignore tags creted by Maven and Gradle release plugins, perhaps you want this: <br>
   * <code>
   * ^\[maven-release-plugin\].*|^\[Gradle Release Plugin\].*|^Merge.*
   * </code><br>
   * <br>
   * Remember to escape it, if added to the json-file it would look like this:<br>
   * <code>
   * ^\\[maven-release-plugin\\].*|^\\[Gradle Release Plugin\\].*|^Merge.*
   * </code>
   */
  private String ignoreCommitsIfMessageMatches;
  /**
   * A date that is evaluated on the commit time of each commit. If the commit is older than the
   * point in time given, then it will be filtered out and not included in the changelog. <br>
   * See {@link SimpleDateFormat}.
   */
  private Date ignoreCommitsIfOlderThan;
  /**
   * Some commits may not be included in any tag. Commits that not released yet may not be tagged.
   * This is a "virtual tag", added to {@link Changelog#getTags()}, that includes those commits. A
   * fitting value may be "Next release".
   */
  private String untaggedName;
  /**
   * Path of template-file to use. It is a Mustache (https://mustache.github.io/) template. Supplied
   * with the context of {@link Changelog}.
   */
  private String templatePath;
  /**
   * Path to the base directory for template partial files. If not null, handlebars will be
   * configured with a FileTemplateLoader with this as base directory.
   */
  private String templateBaseDir;
  /** The filename suffix of template partial files. Requires "templateBaseDir" to be set. */
  private String templateSuffix;
  /**
   * Your tags may look something like <code>git-changelog-maven-plugin-1.6</code>. But in the
   * changelog you just want <code>1.6</code>. With this regular expression, the numbering can be
   * extracted from the tag name.<br>
   * <code>/([^-]+?)$</code>
   */
  private String readableTagName;
  /** Format of dates, see {@link SimpleDateFormat}. */
  private String dateFormat;
  /**
   * This is a "virtual issue" that is added to {@link Changelog#getIssues()}. It contains all
   * commits that has no issue in the commit comment. This could be used as a "wall of shame"
   * listing commiters that did not tag there commits with an issue.
   */
  private String noIssueName;
  /**
   * When date of commits are translated to a string, this timezone is used.<br>
   * <code>UTC</code>
   */
  private String timeZone;
  /**
   * If true, the changelog will not contain the issue in the commit comment. If your changelog is
   * grouped by issues, you may want this to be true. If not grouped by issue, perhaps false.
   */
  private boolean removeIssueFromMessage;
  /** Use any configured feature with Jira. */
  private boolean jiraEnabled;
  /**
   * URL pointing at your JIRA server. When configured, the {@link Issue#getTitle()} will be
   * populated with title from JIRA.<br>
   * <code>https://jiraserver/jira</code>
   */
  private String jiraServer;
  /**
   * Pattern to recognize JIRA:s. <code>\b[a-zA-Z]([a-zA-Z]+)-([0-9]+)\b</code><br>
   * <br>
   * Or escaped if added to json-file:<br>
   * <code>\\b[a-zA-Z]([a-zA-Z]+)-([0-9]+)\\b</code>
   */
  private String jiraIssuePattern;
  /** Authenticate to JIRA. */
  private String jiraUsername;
  /** Authenticate to JIRA. */
  private String jiraPassword;
  /** Authenticate to JIRA. */
  private String jiraToken;
  /** Authenticate to JIRA. */
  private String jiraBearer;

  /** Use any configured feature with Redmine. */
  private boolean redmineEnabled;
  /**
   * URL pointing at your Redmine server. When configured, the {@link Issue#getTitle()} will be
   * populated with title from Redmine.<br>
   * <code>https://redmine/redmine</code>
   */
  private String redmineServer;
  /** Pattern to recognize Redmine:s. <code>#([0-9]+)</code> */
  private String redmineIssuePattern;
  /** Authenticate to Redmine. */
  private String redmineUsername;
  /** Authenticate to Redmine. */
  private String redminePassword;
  /** Authenticate to Redmine whith API_KEY */
  private String redmineToken;
  /** Use any configured feature with Github. */
  private boolean gitHubEnabled;
  /**
   * URL pointing at GitHub API. When configured, the {@link Issue#getTitle()} will be populated
   * with title from GitHub.<br>
   * <code>https://api.github.com/repos/tomasbjerre/git-changelog-lib</code>
   */
  private String gitHubApi;
  /**
   * GitHub authentication token. Configure to avoid low rate limits imposed by GitHub in case you
   * have a lot of issues and/or pull requests.<br>
   * <code>https://api.github.com/repos/tomasbjerre/git-changelog-lib</code>
   */
  private String gitHubToken;
  /** Pattern to recognize GitHub:s. <code>#([0-9]+)</code> */
  private String gitHubIssuePattern;
  /**
   * Custom issues are added to support any kind of issue management, perhaps something that is
   * internal to your project. See {@link SettingsIssue}.
   */
  private List<SettingsIssue> customIssues;
  /**
   * Extended variables is simply a key-value mapping of variables that are made available in the
   * template. Is used, for example, by the Bitbucket plugin to supply some internal variables to
   * the changelog context.
   */
  private Map<String, Object> extendedVariables = new HashMap<>();

  /**
   * Extended headers is simply a key-value mapping of headers that will be passed to REST request.
   * Is used, for example, to bypass 2-factor authentication.
   */
  private Map<String, String> extendedRestHeaders;

  /** Commits that don't have any issue in their commit message will not be included. */
  private boolean ignoreCommitsWithoutIssue;

  /** Use any configured feature with Gitlab. */
  private boolean gitLabEnabled;
  /** GitLab server URL, like https://gitlab.com/. */
  private String gitLabServer;

  private String gitLabToken;

  /** Pattern to recognize GitLab:s. <code>#([0-9]+)</code> */
  private String gitLabIssuePattern;

  /**
   * Like: tomas.bjerre85/violations-test for this repo:
   * https://gitlab.com/tomas.bjerre85/violations-test
   */
  private String gitLabProjectName;

  /** Regular expression to use when determining next semantic version based on commits. */
  private String semanticMajorPattern = null;

  /** Regular expression to use when determining next semantic version based on commits. */
  private String semanticMinorPattern = null;

  /** Regular expression to use when determining next semantic version based on commits. */
  private String semanticPatchPattern;

  /** Integrate with services to get more details about issues. */
  private boolean useIntegrations;

  public String getSubDirFilter() {
    return ofNullable(this.subDirFilter).orElse("");
  }

  public void setPathFilter(final String subDirFilter) {
    this.subDirFilter = subDirFilter;
  }

  private String subDirFilter;

  public Settings() {}

  public void setCustomIssues(final List<SettingsIssue> customIssues) {
    this.customIssues = customIssues;
  }

  public void setFromRef(final String fromRef) {
    if (fromRef == null || fromRef.trim().isEmpty()) {
      this.fromRef = null;
    } else {
      this.fromRef = fromRef.trim();
    }
  }

  public void setToRef(final String toRef) {
    if (toRef == null || toRef.trim().isEmpty()) {
      this.toRef = null;
    } else {
      this.toRef = toRef.trim();
    }
  }

  public Optional<String> getFromRef() {
    return ofNullable(this.fromRef);
  }

  public Optional<String> getToRef() {
    return ofNullable(this.toRef);
  }

  public void setFromRepo(final String fromRepo) {
    this.fromRepo = fromRepo;
  }

  public String getFromRepo() {
    return ofNullable(this.fromRepo).orElse(".");
  }

  public void setIgnoreCommitsIfMessageMatches(final String ignoreCommitsIfMessageMatches) {
    this.ignoreCommitsIfMessageMatches = ignoreCommitsIfMessageMatches;
  }

  public void setIgnoreTagsIfNameMatches(final String ignoreTagsIfNameMatches) {
    this.ignoreTagsIfNameMatches = ignoreTagsIfNameMatches;
  }

  public void setIgnoreCommitsIfOlderThan(final Date ignoreCommitsIfOlderThan) {
    if (ignoreCommitsIfOlderThan != null) {
      this.ignoreCommitsIfOlderThan = new Date(ignoreCommitsIfOlderThan.getTime());
    } else {
      this.ignoreCommitsIfOlderThan = null;
    }
  }

  public void setJiraIssuePattern(final String jiraIssuePattern) {
    this.jiraIssuePattern = jiraIssuePattern;
  }

  public void setJiraServer(final String jiraServer) {
    this.jiraServer = jiraServer;
  }

  public void setRedmineIssuePattern(final String redmineIssuePattern) {
    this.redmineIssuePattern = redmineIssuePattern;
  }

  public void setRedmineServer(final String redmineServer) {
    this.redmineServer = redmineServer;
  }

  public void addCustomIssue(final SettingsIssue customIssue) {
    if (this.customIssues == null) {
      this.customIssues = new ArrayList<>();
    }
    this.customIssues.add(customIssue);
  }

  public List<SettingsIssue> getCustomIssues() {
    if (this.customIssues == null) {
      return new ArrayList<>();
    } else {
      return this.customIssues;
    }
  }

  public String getIgnoreCommitsIfMessageMatches() {
    return ofNullable(this.ignoreCommitsIfMessageMatches).orElse(DEFAULT_IGNORE_COMMITS_REGEXP);
  }

  public Optional<Date> getIgnoreCommitsIfOlderThan() {
    return ofNullable(this.ignoreCommitsIfOlderThan);
  }

  public String getJiraIssuePattern() {
    return ofNullable(this.jiraIssuePattern).orElse(DEFAULT_JIRA_ISSUE_PATTEN);
  }

  public Optional<String> getJiraServer() {
    return ofNullable(this.jiraServer);
  }

  public String getRedmineIssuePattern() {
    return ofNullable(this.redmineIssuePattern).orElse(DEFAULT_REDMINE_ISSUE_PATTEN);
  }

  public Optional<String> getRedmineServer() {
    return ofNullable(this.redmineServer);
  }

  public static Settings fromFile(final URL url) {
    try {
      return gson.fromJson(
          new String(Files.readAllBytes(Paths.get(url.toURI())), UTF_8), Settings.class);
    } catch (final Exception e) {
      throw new RuntimeException("Cannot read " + url, e);
    }
  }

  public void setFromCommit(final String fromCommit) {
    this.fromCommit = fromCommit;
  }

  public void setToCommit(final String toCommit) {
    this.toCommit = toCommit;
  }

  public Optional<String> getFromCommit() {
    return ofNullable(emptyToNull(this.fromCommit));
  }

  public Optional<String> getToCommit() {
    return ofNullable(emptyToNull(this.toCommit));
  }

  public String getUntaggedName() {
    return ofNullable(this.untaggedName).orElse(DEFAULT_UNTAGGED_NAME);
  }

  public Optional<String> getIgnoreTagsIfNameMatches() {
    return ofNullable(this.ignoreTagsIfNameMatches);
  }

  public void setUntaggedName(final String untaggedName) {
    this.untaggedName = untaggedName;
  }

  public String getTemplatePath() {
    return ofNullable(this.templatePath).orElse("changelog.mustache");
  }

  public void setTemplatePath(final String templatePath) {
    this.templatePath = templatePath;
  }

  public String getTemplateBaseDir() {
    return this.templateBaseDir;
  }

  public void setTemplateBaseDir(final String templateBaseDir) {
    this.templateBaseDir = templateBaseDir;
  }

  public String getTemplateSuffix() {
    return this.templateSuffix;
  }

  public void setTemplateSuffix(final String templateSuffix) {
    this.templateSuffix = templateSuffix;
  }

  public String getReadableTagName() {
    return ofNullable(this.readableTagName).orElse(DEFAULT_READABLE_TAG_NAME);
  }

  public String getDateFormat() {
    return ofNullable(this.dateFormat).orElse(DEFAULT_DATEFORMAT);
  }

  public void setDateFormat(final String dateFormat) {
    this.dateFormat = dateFormat;
  }

  public void setNoIssueName(final String noIssueName) {
    this.noIssueName = noIssueName;
  }

  public void setReadableTagName(final String readableTagName) {
    this.readableTagName = readableTagName;
  }

  public String getNoIssueName() {
    return ofNullable(this.noIssueName).orElse(DEFAULT_NO_ISSUE_NAME);
  }

  public void setTimeZone(final String timeZone) {
    this.timeZone = timeZone;
  }

  public String getTimeZone() {
    return ofNullable(this.timeZone).orElse(DEFAULT_TIMEZONE);
  }

  public static Settings defaultSettings() {
    final Settings s = new Settings();
    s.setFromRepo(".");
    s.setFromCommit(ZERO_COMMIT);
    s.setToRef("refs/heads/master");
    s.setIgnoreCommitsIfMessageMatches("^Merge.*");
    s.setTemplateSuffix(".hbs");
    s.setReadableTagName("/([^/]+?)$");
    s.setDateFormat("YYYY-MM-dd HH:mm:ss");
    s.setUntaggedName("No tag");
    s.setNoIssueName("No issue");
    s.setTimeZone("UTC");
    s.setRemoveIssueFromMessage(true);
    s.setJiraIssuePattern("\\\\b[a-zA-Z]([a-zA-Z]+)-([0-9]+)\\\\b");
    return s;
  }

  public void setRemoveIssueFromMessage(final boolean removeIssueFromMessage) {
    this.removeIssueFromMessage = removeIssueFromMessage;
  }

  public Boolean removeIssueFromMessage() {
    return ofNullable(this.removeIssueFromMessage).orElse(DEFAULT_REMOVE_ISSUE);
  }

  public Optional<String> getGitHubApi() {
    return ofNullable(this.gitHubApi);
  }

  public Optional<String> getGitHubToken() {
    return ofNullable(this.gitHubToken);
  }

  public void setGitHubApi(final String gitHubApi) {
    this.gitHubApi = gitHubApi;
  }

  public void setGitHubToken(final String gitHubToken) {
    this.gitHubToken = gitHubToken;
  }

  public void setGitHubIssuePattern(final String gitHubIssuePattern) {
    this.gitHubIssuePattern = gitHubIssuePattern;
  }

  public String getGitHubIssuePattern() {
    return ofNullable(this.gitHubIssuePattern).orElse(DEFAULT_GITHUB_ISSUE_PATTERN);
  }

  public Optional<String> getJiraUsername() {
    return ofNullable(this.jiraUsername);
  }

  public void setJiraPassword(final String jiraPassword) {
    this.jiraPassword = jiraPassword;
  }

  public void setJiraToken(final String jiraToken) {
    this.jiraToken = jiraToken;
  }

  public void setJiraBearer(final String jiraBearer) {
    this.jiraBearer = jiraBearer;
  }

  public void setJiraUsername(final String jiraUsername) {
    this.jiraUsername = jiraUsername;
  }

  public Optional<String> getJiraPassword() {
    return ofNullable(this.jiraPassword);
  }

  public Optional<String> getJiraToken() {
    return ofNullable(this.jiraToken);
  }

  public Optional<String> getJiraBearer() {
    return ofNullable(this.jiraBearer);
  }

  public Optional<String> getRedmineUsername() {
    return ofNullable(this.redmineUsername);
  }

  public void setRedminePassword(final String redminePassword) {
    this.redminePassword = redminePassword;
  }

  public void setRedmineToken(final String redmineToken) {
    this.redmineToken = redmineToken;
  }

  public void setRedmineUsername(final String redmineUsername) {
    this.redmineUsername = redmineUsername;
  }

  public Optional<String> getRedminePassword() {
    return ofNullable(this.redminePassword);
  }

  public Optional<String> getRedmineToken() {
    return ofNullable(this.redmineToken);
  }

  public void setExtendedVariables(final Map<String, Object> extendedVariables) {
    this.extendedVariables = extendedVariables;
  }

  public Map<String, Object> getExtendedVariables() {
    return this.extendedVariables;
  }

  public Map<String, String> getExtendedRestHeaders() {
    return this.extendedRestHeaders;
  }

  public void setExtendedRestHeaders(final Map<String, String> extendedRestHeaders) {
    this.extendedRestHeaders = extendedRestHeaders;
  }

  public void setIgnoreCommitsWithoutIssue(final boolean ignoreCommitsWithoutIssue) {
    this.ignoreCommitsWithoutIssue = ignoreCommitsWithoutIssue;
  }

  public boolean ignoreCommitsWithoutIssue() {
    return this.ignoreCommitsWithoutIssue;
  }

  public void setGitLabIssuePattern(final String gitLabIssuePattern) {
    this.gitLabIssuePattern = gitLabIssuePattern;
  }

  public void setGitLabProjectName(final String gitLabProjectName) {
    this.gitLabProjectName = gitLabProjectName;
  }

  public void setGitLabServer(final String gitLabServer) {
    this.gitLabServer = gitLabServer;
  }

  public void setGitLabToken(final String gitLabToken) {
    this.gitLabToken = gitLabToken;
  }

  public Optional<String> getGitLabServer() {
    return ofNullable(this.gitLabServer);
  }

  public Optional<String> getGitLabToken() {
    return ofNullable(this.gitLabToken);
  }

  public String getGitLabIssuePattern() {
    return ofNullable(this.gitLabIssuePattern).orElse(DEFAULT_GITLAB_ISSUE_PATTERN);
  }

  public Optional<String> getGitLabProjectName() {
    return ofNullable(this.gitLabProjectName);
  }

  public Optional<String> getSemanticMajorPattern() {
    return ofNullable(this.semanticMajorPattern);
  }

  public void setSemanticMajorPattern(final String semanticMajorPattern) {
    this.semanticMajorPattern = this.isRegexp(semanticMajorPattern, "semanticMajorPattern");
  }

  public String getSemanticMinorPattern() {
    return ofNullable(this.semanticMinorPattern).orElse(DEFAULT_MINOR_PATTERN);
  }

  public void setSemanticMinorPattern(final String semanticMinorPattern) {
    this.semanticMinorPattern = this.isRegexp(semanticMinorPattern, "semanticMinorPattern");
  }

  public String getSemanticPatchPattern() {
    return ofNullable(this.semanticPatchPattern).orElse(DEFAULT_PATCH_PATTERN);
  }

  public void setSemanticPatchPattern(final String semanticPatchPattern) {
    this.semanticPatchPattern = this.isRegexp(semanticPatchPattern, "semanticPatchPattern");
  }

  private String isRegexp(final String pattern, final String string) {
    try {
      Pattern.compile(pattern);
    } catch (final PatternSyntaxException e) {
      throw new RuntimeException(pattern + " in " + string + " is not valid regexp.");
    }
    return pattern;
  }

  public void setGitHubEnabled(final boolean githubEnabled) {
    this.gitHubEnabled = githubEnabled;
  }

  public void setGitLabEnabled(final boolean gitlabEnabled) {
    this.gitLabEnabled = gitlabEnabled;
  }

  public void setJiraEnabled(final boolean jiraEnabled) {
    this.jiraEnabled = jiraEnabled;
  }

  public void setRedmineEnabled(final boolean redmineEnabled) {
    this.redmineEnabled = redmineEnabled;
  }

  public boolean isGitHubEnabled() {
    return this.gitHubEnabled;
  }

  public boolean isGitLabEnabled() {
    return this.gitLabEnabled;
  }

  public boolean isJiraEnabled() {
    return this.jiraEnabled;
  }

  public boolean isRedmineEnabled() {
    return this.redmineEnabled;
  }

  public void setUseIntegrations(final boolean useIntegrations) {
    this.useIntegrations = useIntegrations;
  }

  public boolean isUseIntegrations() {
    return this.useIntegrations;
  }
} public class SettingsIssue implements Serializable {
  private static final long serialVersionUID = -658106272421601880L;
  /** Use {@link SettingsIssueType#CUSTOM} when adding custom issues. */
  private final SettingsIssueType type;
  /** Name of the issue manager. This is the {@link Issue#getName()}. Perhaps "GitHub" or "JIRA". */
  private final String name;
  /**
   * Title of the issues. Optional and can, for example, be used when when grouping issues per issue
   * type.
   */
  private final String title;
  /**
   * Regular expression that is evaluated in commit comment. If true, the commit is available in
   * {@link Issue#getCommits()}.
   */
  private final String pattern;
  /**
   * Link pointing at the issue. It supports variables like:<br>
   * <code>${PATTERN_GROUP}</code><br>
   * <code>${PATTERN_GROUP_1}</code><br>
   */
  private final String link;

  public SettingsIssue(
      final String name, final String pattern, final String link, final String title) {
    this.type = CUSTOM;
    this.name = checkNotNull(name, "name");
    this.pattern = checkNotNull(pattern, "pattern");
    this.link = link;
    this.title = title;
  }

  public SettingsIssue(
      final SettingsIssueType type,
      final String name,
      final String pattern,
      final String link,
      final String title) {
    this.type = checkNotNull(type, "type");
    this.name = checkNotNull(name, "name");
    this.pattern = checkNotNull(pattern, "pattern");
    this.link = link;
    this.title = title;
  }

  public SettingsIssueType getType() {
    return this.type;
  }

  public Optional<String> getLink() {
    return ofNullable(this.link);
  }

  public Optional<String> getTitle() {
    return ofNullable(this.title);
  }

  public String getName() {
    return this.name;
  }

  public String getPattern() {
    return this.pattern;
  }
}
import static java.util.regex.Pattern.compile;
import static java.util.stream.Collectors.toList;
import static org.slf4j.LoggerFactory.getLogger;
import static se.bjurr.gitchangelog.internal.integrations.github.GitHubServiceFactory.getGitHubService;
import static se.bjurr.gitchangelog.internal.settings.SettingsIssueType.CUSTOM;
import static se.bjurr.gitchangelog.internal.settings.SettingsIssueType.GITHUB;
import static se.bjurr.gitchangelog.internal.settings.SettingsIssueType.GITLAB;
import static se.bjurr.gitchangelog.internal.settings.SettingsIssueType.JIRA;
import static se.bjurr.gitchangelog.internal.settings.SettingsIssueType.NOISSUE;
import static se.bjurr.gitchangelog.internal.settings.SettingsIssueType.REDMINE;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import org.slf4j.Logger;
import se.bjurr.gitchangelog.api.exceptions.GitChangelogIntegrationException;
import se.bjurr.gitchangelog.internal.git.model.GitCommit;
import se.bjurr.gitchangelog.internal.integrations.github.GitHubHelper;
import se.bjurr.gitchangelog.internal.integrations.github.GitHubIssue;
import se.bjurr.gitchangelog.internal.integrations.github.GitHubLabel;
import se.bjurr.gitchangelog.internal.integrations.gitlab.GitLabClient;
import se.bjurr.gitchangelog.internal.integrations.gitlab.GitLabIssue;
import se.bjurr.gitchangelog.internal.integrations.jira.JiraClient;
import se.bjurr.gitchangelog.internal.integrations.jira.JiraClientFactory;
import se.bjurr.gitchangelog.internal.integrations.jira.JiraIssue;
import se.bjurr.gitchangelog.internal.integrations.redmine.RedmineClient;
import se.bjurr.gitchangelog.internal.integrations.redmine.RedmineClientFactory;
import se.bjurr.gitchangelog.internal.integrations.redmine.RedmineIssue;
import se.bjurr.gitchangelog.internal.model.ParsedIssue;
import se.bjurr.gitchangelog.internal.settings.IssuesUtil;
import se.bjurr.gitchangelog.internal.settings.Settings;
import se.bjurr.gitchangelog.internal.settings.SettingsIssue;
package se.bjurr.gitchangelog.internal.issues;



public class IssueParser {

  private static final Logger LOG = getLogger(IssueParser.class);

  private final List<GitCommit> commits;
  private final Settings settings;

  public IssueParser(final Settings settings, final List<GitCommit> commits) {
    this.settings = settings;
    this.commits = commits;
  }

  public Settings getSettings() {
    return this.settings;
  }

  public List<GitCommit> getCommits() {
    return this.commits;
  }

  public List<ParsedIssue> parseForIssues(final boolean useIntegrations) {
    final Map<String, ParsedIssue> parsedIssuePerIssue = new HashMap<>();
    final GitHubHelper gitHubHelper = useIntegrations ? this.createGitHubClient() : null;
    final JiraClient jiraClient = useIntegrations ? this.createJiraClient() : null;
    final RedmineClient redmineClient = useIntegrations ? this.createRedmineClient() : null;
    final GitLabClient gitLabClient = useIntegrations ? this.createGitLabClient() : null;
    final List<SettingsIssue> patterns = new IssuesUtil(this.settings).getIssues();