diff --git a/README.md b/README.md index 1a7e487..1a467d1 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,19 @@ repo.getIssues(false).ifPresent(issueData -> issueData.forEach(issue -> { System.out.println(comment.user.username + ": " + comment.body)); })); ``` + +### Further data processing + +The data extracted by this tool can be further processed, for example using the `run-issues.py` script from the tool [`codeface-extraction`](https://github.com/se-sic/codeface-extraction). This organizes and unifies the issue data into a single csv-like .list file. It also allows for synchronization with data from other data extraction tools, such as `codeface`. + +### `referenced` events + +`referenced` events are events generated in an issue if a commit references that issue in its commit message. The intended behavior is that the event is present in the issue's event data, and the commit is again present in the related commits of the issue. This does not work if it is not possible to fetch that commit. In this case, the event still exists, but it contains a link to a commit that the api cannot resolve, meaning that no data about the commit can be accessed. +Known causes of this include: + +- a commit was rebased and changed/removed +- an external repository was deleted +- the commit's branch was deleted + +Note that the commit might still be reachable until the automatic garbage collection has removed it from the remote repository. +In itself, this is not problematic. However, when further processing the data using `codeface-extraction`, this may lead to these `referenced` events being present in the final data, even though they should be filtered out as part of the issue processing. diff --git a/src/de/uni_passau/fim/gitwrapper/EventData.java b/src/de/uni_passau/fim/gitwrapper/EventData.java index 622fe1e..9560691 100644 --- a/src/de/uni_passau/fim/gitwrapper/EventData.java +++ b/src/de/uni_passau/fim/gitwrapper/EventData.java @@ -1,6 +1,8 @@ /** * Copyright (C) 2016-2018 Florian Heck * Copyright (C) 2019 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach + * Copyright (C) 2025 Shiraz Jafri * * This file is part of GitHubWrapper. * @@ -33,6 +35,7 @@ public abstract class EventData { UserData user; OffsetDateTime created_at; String event; + Long id; /** * The User that created the Event. @@ -193,4 +196,52 @@ public UserData getAssigner() { return assigner; } } + + /** + * An Event generated by changing the state of an issue. + */ + public class StateChangedEventData extends EventData { + + @Expose(deserialize = false) + Commit commit; + StateReason state_reason; + + /** + * The commit references. + */ + public Commit getCommit() { + return commit; + } + + /** + * The reason for the state change. + */ + public StateReason getStateReason() { + return state_reason; + } + } + + /** + * An Event generated by changing the type of an issue. + */ + public class IssueTypeChangedEventData extends EventData { + } + + /** + * An Event generated by changing the parent issue of an issue. + */ + public class ParentIssueChangedEventData extends EventData { + } + + /** + * An Event generated by changing the sub-issue of an issue. + */ + public class SubIssueChangedEventData extends EventData { + } + + /** + * An Event generated by connecting to a repository. + */ + public class ConnectedEventData extends EventData { + } } diff --git a/src/de/uni_passau/fim/gitwrapper/EventDataProcessor.java b/src/de/uni_passau/fim/gitwrapper/EventDataProcessor.java index 40d49a0..1b14a09 100644 --- a/src/de/uni_passau/fim/gitwrapper/EventDataProcessor.java +++ b/src/de/uni_passau/fim/gitwrapper/EventDataProcessor.java @@ -1,6 +1,8 @@ /** * Copyright (C) 2016-2018 Florian Heck * Copyright (C) 2019 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach + * Copyright (C) 2025 Shiraz Jafri * * This file is part of GitHubWrapper. * @@ -43,7 +45,16 @@ class EventDataProcessor implements JsonDeserializer, JsonSerializer< map.put("unlabeled", EventData.LabeledEventData.class); map.put("referenced", EventData.ReferencedEventData.class); map.put("merged", EventData.ReferencedEventData.class); - map.put("closed", EventData.ReferencedEventData.class); + map.put("closed", EventData.StateChangedEventData.class); + map.put("reopened", EventData.StateChangedEventData.class); + map.put("connected", EventData.ConnectedEventData.class); + map.put("issue_type_added", EventData.IssueTypeChangedEventData.class); + map.put("issue_type_changed", EventData.IssueTypeChangedEventData.class); + map.put("issue_type_removed", EventData.IssueTypeChangedEventData.class); + map.put("parent_issue_added", EventData.ParentIssueChangedEventData.class); + map.put("parent_issue_removed", EventData.ParentIssueChangedEventData.class); + map.put("sub_issue_added", EventData.SubIssueChangedEventData.class); + map.put("sub_issue_removed", EventData.SubIssueChangedEventData.class); map.put("review_requested", EventData.RequestedReviewEventData.class); map.put("review_request_removed", EventData.RequestedReviewEventData.class); map.put("review_dismissed", EventData.DismissedReviewEventData.class); @@ -86,8 +97,12 @@ public void postDeserialize(EventData.ReferencedEventData result, JsonElement sr } result.commit = repo.getGithubCommit(hash.getAsString()).orElseGet(() -> { - LOG.warning("Found commit unknown to GitHub and local git repo: " + hash); - return null; + LOG.warning("Found commit unknown to GitHub and local git repo: " + hash + " Retry using url..."); + JsonElement url = src.getAsJsonObject().get("commit_url"); + return repo.getGithubCommitUrl(hash.getAsString(), url.getAsString()).orElseGet(() -> { + LOG.warning("Could not find commit: " + hash); + return null; + }); }); } @@ -158,4 +173,76 @@ public void postDeserialize(EventData.AssignedEventData result, JsonElement src, @Override public void postSerialize(JsonElement result, EventData.AssignedEventData src, Gson gson) { } } + + /** + * Processor for state change events. + */ + static class StateChangedEventProcessor implements PostProcessor { + + private GitHubRepository repo; + + /** + * Creates a new EventDataProcessor for the given repo. + * + * @param repo + * the repo + */ + StateChangedEventProcessor(GitHubRepository repo) { + this.repo = repo; + } + + @Override + public void postDeserialize(EventData.StateChangedEventData result, JsonElement src, Gson gson) { + JsonElement stateReasonElement = src.getAsJsonObject().get("state_reason"); + String stateReasonValue = (stateReasonElement != null && !stateReasonElement.isJsonNull()) + ? stateReasonElement.getAsString() + : null; + result.state_reason = StateReason.getFromString(stateReasonValue); + + JsonElement hash = src.getAsJsonObject().get("commit_id"); + if (hash.isJsonNull()) { + return; + } + + result.commit = repo.getGithubCommit(hash.getAsString()).orElseGet(() -> { + LOG.warning("Found commit unknown to GitHub and local git repo: " + hash + " Retry using url..."); + JsonElement url = src.getAsJsonObject().get("commit_url"); + return repo.getGithubCommitUrl(hash.getAsString(), url.getAsString()).orElseGet(() -> { + LOG.warning("Could not find commit: " + hash); + return null; + }); + }); + } + + @Override + public void postSerialize(JsonElement result, EventData.StateChangedEventData src, Gson gson) { } + } + + /** + * Processor for issue type change events. + */ + static class IssueTypeChangedEventProcessor implements PostProcessor { + + @Override + public void postDeserialize(EventData.IssueTypeChangedEventData result, JsonElement src, Gson gson) { + } + + @Override + public void postSerialize(JsonElement result, EventData.IssueTypeChangedEventData src, Gson gson) { + } + } + + /** + * Processor for connected events. + */ + static class ConnectedEventProcessor implements PostProcessor { + + @Override + public void postDeserialize(EventData.ConnectedEventData result, JsonElement src, Gson gson) { + } + + @Override + public void postSerialize(JsonElement result, EventData.ConnectedEventData src, Gson gson) { + } + } } diff --git a/src/de/uni_passau/fim/gitwrapper/GitHubCommit.java b/src/de/uni_passau/fim/gitwrapper/GitHubCommit.java index 16a2429..29f8501 100644 --- a/src/de/uni_passau/fim/gitwrapper/GitHubCommit.java +++ b/src/de/uni_passau/fim/gitwrapper/GitHubCommit.java @@ -1,5 +1,6 @@ /** * Copyright (C) 2019 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach * * This file is part of GitHubWrapper. * @@ -26,6 +27,7 @@ public class GitHubCommit extends Commit { private String authorUsername; private String committerUsername; private boolean addedToPullRequest = false; + private boolean external = false; /** * Constructs a new {@link GitHubCommit} with the given id made in the repo. @@ -119,4 +121,23 @@ public boolean isAddedToPullRequest() { void setAddedToPullRequest(boolean added) { this.addedToPullRequest = added; } + + /** + * Returns whether this commit is an external commit. + * + * @return whether this commit is an external commit + */ + boolean isExternal() { + return this.external; + } + + /** + * Sets whether this commit is an external commit + * + * @param external this commit is an external commit + */ + void setExternal(boolean external) { + this.external = external; + } + } diff --git a/src/de/uni_passau/fim/gitwrapper/GitHubRepository.java b/src/de/uni_passau/fim/gitwrapper/GitHubRepository.java index 335f6cf..b97978d 100644 --- a/src/de/uni_passau/fim/gitwrapper/GitHubRepository.java +++ b/src/de/uni_passau/fim/gitwrapper/GitHubRepository.java @@ -2,6 +2,8 @@ * Copyright (C) 2016-2020 Florian Heck * Copyright (C) 2018 Claus Hunsen * Copyright (C) 2019-2021 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach + * Copyright (C) 2025 Shiraz Jafri * * This file is part of GitHubWrapper. * @@ -241,6 +243,9 @@ public GitHubRepository(String url, File dir, GitWrapper git, List oauth gfb.registerPostProcessor(EventData.LabeledEventData.class, new EventDataProcessor.LabeledEventProcessor()); gfb.registerPostProcessor(EventData.DismissedReviewEventData.class, new EventDataProcessor.DismissedReviewEventProcessor()); gfb.registerPostProcessor(EventData.AssignedEventData.class, new EventDataProcessor.AssignedEventProcessor()); + gfb.registerPostProcessor(EventData.StateChangedEventData.class, new EventDataProcessor.StateChangedEventProcessor(this)); + gfb.registerPostProcessor(EventData.IssueTypeChangedEventData.class, new EventDataProcessor.IssueTypeChangedEventProcessor()); + gfb.registerPostProcessor(EventData.ConnectedEventData.class, new EventDataProcessor.ConnectedEventProcessor()); gfb.registerPostProcessor(ReviewData.ReviewInitialCommentData.class, new ReviewDataProcessor.ReviewInitialCommentDataProcessor(this)); GsonBuilder gb = gfb.createGsonBuilder(); gb.registerTypeAdapter(Commit.class, new CommitProcessor(this, userProcessor)); @@ -352,6 +357,8 @@ public Optional> getIssues(boolean includePullRequests, OffsetDa } else timeLimit = ""; Type finalType = type; + // For debugging, you may add additional parameters to the string. For example, '/issues?creator=username&state=all' + // will fetch issues created by the specified and all related issues and commits. getJSONStringFromPath("/issues?state=all" + timeLimit).map(json -> { List data; try { @@ -367,7 +374,7 @@ public Optional> getIssues(boolean includePullRequests, OffsetDa threadPool.submit(() -> data.parallelStream().forEach(IssueData::freeze)); } catch (JsonSyntaxException e) { - LOG.warning("Encountered invalid JSON: " + json); + LOG.warning("Encountered invalid JSON: " + json + "\n\n" + e.getMessage() + "\n\n" + e); return null; } return data; @@ -1028,6 +1035,38 @@ Optional getGithubCommit(String hash) { }); } + Optional getGithubCommitUrl(String hash, String url) { + if (offline.get()) { + return Optional.of(getGHCommitUnchecked(DummyCommit.DUMMY_COMMIT_ID)); + } else { + try { + Optional res = getJSONStringFromURL(url).map(commitInfo -> + gson.fromJson(commitInfo, new TypeToken() {}.getType())); + checkedHashes.put(hash, res); + if (res.isPresent()) { + res.get().setExternal(true); + } + return res; + } catch (JsonSyntaxException e) { + /* For whatever reason, the JSON String is malformed, perhaps due to ill-encoded characters + * in patches within the files element of the JSON String. + * Due to that, get the JSON String again and remove the content of the files element of the + * JSON String, as it is not needed for further processing. + */ + LOG.info("Malformed JSON String when querying data for commit " + url + ". Neglect files element."); + String jsonStringFromURL = getJSONStringFromURL(url).get(); + jsonStringFromURL = StringUtils.substringBefore(jsonStringFromURL, "\"files\":["); + jsonStringFromURL = jsonStringFromURL + "\"files\":[]}"; + Optional res = Optional.of(gson.fromJson(jsonStringFromURL, new TypeToken() {}.getType())); + checkedHashes.put(hash, res); + if (res.isPresent()) { + res.get().setExternal(true); + } + return res; + } + } + } + /** * Creates a new Commit with the given data, and tries to fill in the missing data from the local Repository * diff --git a/src/de/uni_passau/fim/gitwrapper/IssueData.java b/src/de/uni_passau/fim/gitwrapper/IssueData.java index 09539c0..3bb67d7 100644 --- a/src/de/uni_passau/fim/gitwrapper/IssueData.java +++ b/src/de/uni_passau/fim/gitwrapper/IssueData.java @@ -1,6 +1,8 @@ /** * Copyright (C) 2016-2018 Florian Heck * Copyright (C) 2019 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach + * Copyright (C) 2025 Shiraz Jafri * * This file is part of GitHubWrapper. * @@ -39,6 +41,7 @@ public class IssueData implements GitHubRepository.IssueDataCached { UserData user; @Expose(deserialize = false) State state; + @Expose(deserialize = false) TypeData type; OffsetDateTime created_at; @Nullable OffsetDateTime closed_at; @@ -51,6 +54,7 @@ public class IssueData implements GitHubRepository.IssueDataCached { private List reviewsList; private List> relatedCommits; List> relatedIssues; + private List subIssues; transient GitHubRepository repo; private transient boolean frozen; @@ -100,6 +104,16 @@ void setRelatedCommits(List> commits) { relatedCommits = commits; } + /** + * Sets a list of sub-issues to this Issue. + * + * @param issues + * the list of issue numbers + */ + void setSubIssues(List issues) { + subIssues = issues; + } + /** * Sets a list of related Issues (rather their numbers) to this Issue * from links containing just issues numbers. @@ -276,6 +290,15 @@ public List> getRelatedCommits() { return relatedCommits; } + /** + * Gets a List of all sub-issues that belong to the Issue. + * + * @return a List of sub-issues in form of a list containing their issue numbers + */ + public List getSubIssues() { + return subIssues; + } + /** * Gets a List of all Issues referenced in the Issue and its Comments. * diff --git a/src/de/uni_passau/fim/gitwrapper/IssueDataProcessor.java b/src/de/uni_passau/fim/gitwrapper/IssueDataProcessor.java index b9ced2f..3cae242 100644 --- a/src/de/uni_passau/fim/gitwrapper/IssueDataProcessor.java +++ b/src/de/uni_passau/fim/gitwrapper/IssueDataProcessor.java @@ -1,6 +1,8 @@ /** * Copyright (C) 2016-2018 Florian Heck * Copyright (C) 2019-2020 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach + * Copyright (C) 2025 Shiraz Jafri * * This file is part of GitHubWrapper. * @@ -76,7 +78,13 @@ private List> parseCommits(IssueData issue) { .filter(eventData -> eventData instanceof EventData.ReferencedEventData) // filter out errors from referencing commits .filter(eventData -> ((EventData.ReferencedEventData) eventData).commit != null) - .map(eventData -> new ReferencedLink<>(Collections.singletonList(((EventData.ReferencedEventData) eventData).commit.getId()), eventData.user, eventData.created_at, "commitReferencesIssue")); + .map(eventData -> { + if (((GitHubCommit) ((EventData.ReferencedEventData) eventData).commit).isExternal()) { + return new ReferencedLink<>(Collections.singletonList(((EventData.ReferencedEventData) eventData).commit.getId()), eventData.user, eventData.created_at, "commitReferencesIssueExternal"); + } else { + return new ReferencedLink<>(Collections.singletonList(((EventData.ReferencedEventData) eventData).commit.getId()), eventData.user, eventData.created_at, "commitReferencesIssue"); + } + }); // Parse commits from reviews and reviews' comments if (issue.isPullRequest()) { @@ -224,6 +232,28 @@ List> parseIssues(IssueData issue, Gson gson) { .collect(Collectors.toList()); } + /** + * Parse subissues + * + * @param issue + * the Issue to analyze + * @param gson + * the Gson used to deserialize + * @return a list of all Issue numbers of sub-issues + */ + List parseSubIssues(IssueData issue, Gson gson) { + + Optional subIssueJson = repo.getJSONStringFromURL(issueBaseUrl + issue.number + "/sub_issues"); + + if (subIssueJson.isEmpty()) { + return new ArrayList(); + } + ArrayList list = gson.fromJson(subIssueJson.get(), new TypeToken>() {}.getType()); + return list.stream().map(s -> (((IssueData) gson.fromJson(s, new TypeToken() {}.getType())).getNumber())) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + /** * Extracts theoretically valid commit hashes from text. * @@ -235,7 +265,17 @@ private List extractSHA1s(String text) { if (text == null) { return Collections.emptyList(); } - Pattern sha1Pattern = Pattern.compile("([0-9a-f]{5,40})"); + + // filter out everything in code block + String[] texts = text.split("```"); + text = ""; + for (int i = 0; i < texts.length; i++) { + if (i % 2 == 0) { + text = text + texts[i]; + } + } + + Pattern sha1Pattern = Pattern.compile("([0-9a-f]{7,40})"); Matcher matcher = sha1Pattern.matcher(text); List sha1s = new ArrayList<>(); @@ -262,6 +302,16 @@ private List extractHashtags(String text, boolean onlyInSameRepo) { } Pattern hashtagPattern; + // filter out everything in code block + String[] texts = text.split("```"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < texts.length; i++) { + if (i % 2 == 0) { + sb.append(texts[i]); + } + } + text = sb.toString(); + if (onlyInSameRepo) { String repoName = repo.getRepoName(); String repoUser = repo.getRepoUser(); @@ -375,10 +425,18 @@ public void postDeserialize(IssueData result, JsonElement src, Gson gson) { // fill in missing data result.state = State.getFromString(src.getAsJsonObject().get("state").getAsString()); + JsonElement typeElement = src.getAsJsonObject().get("type"); + if (typeElement != null && !typeElement.isJsonNull()) { + result.type = gson.fromJson(typeElement, TypeData.class); + } else { + result.type = null; + } + if (result.getCommentsList() == null) { Optional>> comments = repo.getComments(lookup); result.setComments(comments.orElse(Collections.emptyList())); } + if (result.getEventsList() == null) { Optional> events = repo.getEvents(lookup); result.setEvents(events.orElse(Collections.emptyList())); @@ -426,6 +484,10 @@ public void postDeserialize(IssueData result, JsonElement src, Gson gson) { result.setRelatedIssues(parseIssues(result, gson)); } + if (result.getSubIssues() == null) { + result.setSubIssues(parseSubIssues(result, gson)); + } + workingQueue.remove(result.number); } diff --git a/src/de/uni_passau/fim/gitwrapper/ReferencedLinkProcessor.java b/src/de/uni_passau/fim/gitwrapper/ReferencedLinkProcessor.java index 955f94e..e799ffd 100644 --- a/src/de/uni_passau/fim/gitwrapper/ReferencedLinkProcessor.java +++ b/src/de/uni_passau/fim/gitwrapper/ReferencedLinkProcessor.java @@ -1,6 +1,7 @@ /** * Copyright (C) 2018 Florian Heck * Copyright (C) 2019 Thomas Bock + * Copyright (C) 2025 Leo Sendelbach * * This file is part of GitHubWrapper. * @@ -64,6 +65,7 @@ public ReferencedLink deserialize(JsonElement json, Type typeOfT, JsonDeserializ } if (!json.getAsJsonObject().get("body").isJsonNull()) { commentData.body = json.getAsJsonObject().get("body").getAsString(); + commentData.suggestion = commentData.body.toLowerCase().contains("```suggestion"); } else { commentData.body = ""; } @@ -99,6 +101,7 @@ public ReferencedLink deserialize(JsonElement json, Type typeOfT, JsonDeserializ commentData.commit_id = json.getAsJsonObject().get("commit_id").getAsString(); commentData.original_commit_id = json.getAsJsonObject().get("original_commit_id").getAsString(); commentData.body = json.getAsJsonObject().get("body").getAsString(); + commentData.suggestion = commentData.body.toLowerCase().contains("```suggestion"); result.target = commentData; } else { // normal comment @@ -158,6 +161,7 @@ public void postSerialize(JsonElement result, ReferencedLink src, Gson gson) { result.getAsJsonObject().addProperty("original_position", ((ReviewCommentData) src.getTarget()).getOriginalPosition()); result.getAsJsonObject().addProperty("commit_id", ((ReviewCommentData) src.getTarget()).getCommitId()); result.getAsJsonObject().addProperty("original_commit_id", ((ReviewCommentData) src.getTarget()).getOriginalCommitId()); + result.getAsJsonObject().addProperty("contains_suggestion", ((ReviewCommentData) src.getTarget()).containsSuggestion()); result.getAsJsonObject().remove("target"); break; case "Integer": diff --git a/src/de/uni_passau/fim/gitwrapper/ReviewCommentData.java b/src/de/uni_passau/fim/gitwrapper/ReviewCommentData.java index 950673a..0181a65 100644 --- a/src/de/uni_passau/fim/gitwrapper/ReviewCommentData.java +++ b/src/de/uni_passau/fim/gitwrapper/ReviewCommentData.java @@ -35,6 +35,7 @@ public class ReviewCommentData { @SerializedName(value = "file", alternate = {"path"}) String file; String body; + boolean suggestion = false; public ReviewCommentData() { } @@ -80,4 +81,11 @@ public String getFile() { public String getBody() { return body; } + + /** + * If the comment contains a suggestion + */ + public boolean containsSuggestion() { + return suggestion; + } } diff --git a/src/de/uni_passau/fim/gitwrapper/StateReason.java b/src/de/uni_passau/fim/gitwrapper/StateReason.java new file mode 100644 index 0000000..b5b0692 --- /dev/null +++ b/src/de/uni_passau/fim/gitwrapper/StateReason.java @@ -0,0 +1,71 @@ +/** + * Copyright (C) 2025 Shiraz Jafri + * Copyright (C) 2025 Leo Sendelbach + * + * This file is part of GitHubWrapper. + * + * GitHubWrapper is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GitHubWrapper is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with GitWrapper. If not, see . + */ +package de.uni_passau.fim.gitwrapper; + +/** + * Enumeration of reasons for the state of an Issue or PullRequest. + */ +public enum StateReason { + /** + * Denotes that the Issue or PullRequest was completed. + */ + COMPLETED, + + /** + * Denotes that the Issue or PullRequest was reopened. + */ + REOPENED, + + /** + * Denotes that the Issue or PullRequest was not planned. + */ + NOT_PLANNED, + + /** + * Denotes that the Issue or PullRequest was a duplicate. + */ + DUPLICATE; + + /** + * Gets the StateReason from a string. + * + * @param string the string to convert + * @return the corresponding StateReason, the default case being 'COMPLETED' + */ + public static StateReason getFromString(String string) { + if (string == null) { + return COMPLETED; + } + + switch (string.toLowerCase()) { + case "completed": + return COMPLETED; + case "reopened": + return REOPENED; + case "not_planned": + return NOT_PLANNED; + case "duplicate": + return DUPLICATE; + default: + throw new IllegalArgumentException("Found state reason (" + string + ") that was neither 'completed'," + + "'reopened', 'not_planned', nor 'duplicate'!"); + } + } +} diff --git a/src/de/uni_passau/fim/gitwrapper/TypeData.java b/src/de/uni_passau/fim/gitwrapper/TypeData.java new file mode 100644 index 0000000..50f621e --- /dev/null +++ b/src/de/uni_passau/fim/gitwrapper/TypeData.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2025 Shiraz Jafri + * + * This file is part of GitHubWrapper. + * + * GitHubWrapper is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GitHubWrapper is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with GitWrapper. If not, see . + */ +package de.uni_passau.fim.gitwrapper; + +/** + * Skeleton object for data about issue types from GitHub's API. + */ +public class TypeData { + String name; + String description; + + /** + * The name of the type. + */ + public String getName() { + return name; + } + + /** + * The description of the type. + */ + public String getDescription() { + return description; + } +}