mirror of
https://github.com/jlengrand/github-api.git
synced 2026-04-14 00:11:23 +00:00
Merge branch 'collaborator-permissions' of https://github.com/jimmysombrero/github-api into collaborator-permissions
This commit is contained in:
@@ -88,7 +88,7 @@ public class GHOrganization extends GHPerson {
|
||||
*
|
||||
* <p>
|
||||
* You use the returned builder to set various properties, then call {@link GHCreateRepositoryBuilder#create()} to
|
||||
* finally createa repository.
|
||||
* finally create a repository.
|
||||
*
|
||||
* @param name
|
||||
* the name
|
||||
@@ -386,7 +386,7 @@ public class GHOrganization extends GHPerson {
|
||||
* @throws IOException
|
||||
* the io exception
|
||||
* @deprecated https://developer.github.com/v3/teams/#create-team deprecates permission field use
|
||||
* {@link #createTeam(String, Collection)}
|
||||
* {@link #createTeam(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public GHTeam createTeam(String name, Permission p, Collection<GHRepository> repositories) throws IOException {
|
||||
@@ -412,7 +412,7 @@ public class GHOrganization extends GHPerson {
|
||||
* @throws IOException
|
||||
* the io exception
|
||||
* @deprecated https://developer.github.com/v3/teams/#create-team deprecates permission field use
|
||||
* {@link #createTeam(String, GHRepository...)}
|
||||
* {@link #createTeam(String)}
|
||||
*/
|
||||
@Deprecated
|
||||
public GHTeam createTeam(String name, Permission p, GHRepository... repositories) throws IOException {
|
||||
@@ -429,7 +429,9 @@ public class GHOrganization extends GHPerson {
|
||||
* @return the gh team
|
||||
* @throws IOException
|
||||
* the io exception
|
||||
* @deprecated Use {@link #createTeam(String)} that uses a builder pattern to let you control every aspect.
|
||||
*/
|
||||
@Deprecated
|
||||
public GHTeam createTeam(String name, Collection<GHRepository> repositories) throws IOException {
|
||||
Requester post = root.createRequest().method("POST").with("name", name);
|
||||
List<String> repo_names = new ArrayList<String>();
|
||||
@@ -450,11 +452,28 @@ public class GHOrganization extends GHPerson {
|
||||
* @return the gh team
|
||||
* @throws IOException
|
||||
* the io exception
|
||||
* @deprecated Use {@link #createTeam(String)} that uses a builder pattern to let you control every aspect.
|
||||
*/
|
||||
@Deprecated
|
||||
public GHTeam createTeam(String name, GHRepository... repositories) throws IOException {
|
||||
return createTeam(name, Arrays.asList(repositories));
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a builder that creates a new team.
|
||||
*
|
||||
* <p>
|
||||
* You use the returned builder to set various properties, then call {@link GHTeamBuilder#create()} to finally
|
||||
* create a team.
|
||||
*
|
||||
* @param name
|
||||
* the name
|
||||
* @return the gh create repository builder
|
||||
*/
|
||||
public GHTeamBuilder createTeam(String name) {
|
||||
return new GHTeamBuilder(root, login, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* List up repositories that has some open pull requests.
|
||||
* <p>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
@@ -317,20 +315,17 @@ public class GHRepositoryStatistics {
|
||||
* the io exception
|
||||
*/
|
||||
public List<CodeFrequency> getCodeFrequency() throws IOException {
|
||||
// Map to ArrayLists first, since there are no field names in the
|
||||
// Map to arrays first, since there are no field names in the
|
||||
// returned JSON.
|
||||
try {
|
||||
InputStream stream = root.createRequest().withUrlPath(getApiTailUrl("code_frequency")).fetchStream();
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
TypeReference<ArrayList<ArrayList<Integer>>> typeRef = new TypeReference<ArrayList<ArrayList<Integer>>>() {
|
||||
};
|
||||
ArrayList<ArrayList<Integer>> list = mapper.readValue(stream, typeRef);
|
||||
Integer[][] list = root.createRequest()
|
||||
.withUrlPath(getApiTailUrl("code_frequency"))
|
||||
.fetch(Integer[][].class);
|
||||
|
||||
// Convert to proper objects.
|
||||
ArrayList<CodeFrequency> returnList = new ArrayList<CodeFrequency>();
|
||||
for (ArrayList<Integer> item : list) {
|
||||
CodeFrequency cf = new CodeFrequency(item);
|
||||
List<CodeFrequency> returnList = new ArrayList<>();
|
||||
for (Integer[] item : list) {
|
||||
CodeFrequency cf = new CodeFrequency(Arrays.asList(item));
|
||||
returnList.add(cf);
|
||||
}
|
||||
|
||||
@@ -351,7 +346,7 @@ public class GHRepositoryStatistics {
|
||||
private int additions;
|
||||
private int deletions;
|
||||
|
||||
private CodeFrequency(ArrayList<Integer> item) {
|
||||
private CodeFrequency(List<Integer> item) {
|
||||
week = item.get(0);
|
||||
additions = item.get(1);
|
||||
deletions = item.get(2);
|
||||
@@ -462,17 +457,12 @@ public class GHRepositoryStatistics {
|
||||
public List<PunchCardItem> getPunchCard() throws IOException {
|
||||
// Map to ArrayLists first, since there are no field names in the
|
||||
// returned JSON.
|
||||
InputStream stream = root.createRequest().withUrlPath(getApiTailUrl("punch_card")).fetchStream();
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
TypeReference<ArrayList<ArrayList<Integer>>> typeRef = new TypeReference<ArrayList<ArrayList<Integer>>>() {
|
||||
};
|
||||
ArrayList<ArrayList<Integer>> list = mapper.readValue(stream, typeRef);
|
||||
Integer[][] list = root.createRequest().withUrlPath(getApiTailUrl("punch_card")).fetch(Integer[][].class);
|
||||
|
||||
// Convert to proper objects.
|
||||
ArrayList<PunchCardItem> returnList = new ArrayList<PunchCardItem>();
|
||||
for (ArrayList<Integer> item : list) {
|
||||
PunchCardItem pci = new PunchCardItem(item);
|
||||
ArrayList<PunchCardItem> returnList = new ArrayList<>();
|
||||
for (Integer[] item : list) {
|
||||
PunchCardItem pci = new PunchCardItem(Arrays.asList(item));
|
||||
returnList.add(pci);
|
||||
}
|
||||
|
||||
@@ -487,7 +477,7 @@ public class GHRepositoryStatistics {
|
||||
private int hourOfDay;
|
||||
private int numberOfCommits;
|
||||
|
||||
private PunchCardItem(ArrayList<Integer> item) {
|
||||
private PunchCardItem(List<Integer> item) {
|
||||
dayOfWeek = item.get(0);
|
||||
hourOfDay = item.get(1);
|
||||
numberOfCommits = item.get(2);
|
||||
|
||||
@@ -12,12 +12,22 @@ import java.util.TreeMap;
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public class GHTeam implements Refreshable {
|
||||
private String name, permission, slug, description;
|
||||
private String name;
|
||||
private String permission;
|
||||
private String slug;
|
||||
private String description;
|
||||
private Privacy privacy;
|
||||
|
||||
private int id;
|
||||
private GHOrganization organization; // populated by GET /user/teams where Teams+Orgs are returned together
|
||||
|
||||
protected /* final */ GitHub root;
|
||||
|
||||
public enum Privacy {
|
||||
SECRET, // only visible to organization owners and members of this team.
|
||||
CLOSED // visible to all members of this organization.
|
||||
}
|
||||
|
||||
/**
|
||||
* Member's role in a team
|
||||
*/
|
||||
@@ -94,6 +104,15 @@ public class GHTeam implements Refreshable {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the privacy state.
|
||||
*
|
||||
* @return the privacy state.
|
||||
*/
|
||||
public Privacy getPrivacy() {
|
||||
return privacy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets description.
|
||||
*
|
||||
@@ -106,6 +125,18 @@ public class GHTeam implements Refreshable {
|
||||
root.createRequest().method("PATCH").with("description", description).withUrlPath(api("")).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the team's privacy setting.
|
||||
*
|
||||
* @param privacy
|
||||
* the privacy
|
||||
* @throws IOException
|
||||
* the io exception
|
||||
*/
|
||||
public void setPrivacy(Privacy privacy) throws IOException {
|
||||
root.createRequest().method("PATCH").with("privacy", privacy).withUrlPath(api("")).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets id.
|
||||
*
|
||||
|
||||
93
src/main/java/org/kohsuke/github/GHTeamBuilder.java
Normal file
93
src/main/java/org/kohsuke/github/GHTeamBuilder.java
Normal file
@@ -0,0 +1,93 @@
|
||||
package org.kohsuke.github;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Creates a team.
|
||||
*
|
||||
* https://developer.github.com/v3/teams/#create-team
|
||||
*/
|
||||
public class GHTeamBuilder {
|
||||
|
||||
private final GitHub root;
|
||||
protected final Requester builder;
|
||||
private final String orgName;
|
||||
|
||||
public GHTeamBuilder(GitHub root, String orgName, String name) {
|
||||
this.root = root;
|
||||
this.orgName = orgName;
|
||||
this.builder = root.createRequest();
|
||||
this.builder.with("name", name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Description for this team.
|
||||
*
|
||||
* @param description
|
||||
* description of team
|
||||
* @return a builder to continue with building
|
||||
*/
|
||||
public GHTeamBuilder description(String description) {
|
||||
this.builder.with("description", description);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintainers for this team.
|
||||
*
|
||||
* @param maintainers
|
||||
* maintainers of team
|
||||
* @return a builder to continue with building
|
||||
*/
|
||||
public GHTeamBuilder maintainers(String... maintainers) {
|
||||
this.builder.with("maintainers", maintainers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repository names to add this team to.
|
||||
*
|
||||
* @param repoNames
|
||||
* repoNames to add team to
|
||||
* @return a builder to continue with building
|
||||
*/
|
||||
public GHTeamBuilder repositories(String... repoNames) {
|
||||
this.builder.with("repo_names", repoNames);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description for this team
|
||||
*
|
||||
* @param privacy
|
||||
* privacy of team
|
||||
* @return a builder to continue with building
|
||||
*/
|
||||
public GHTeamBuilder privacy(GHTeam.Privacy privacy) {
|
||||
this.builder.with("privacy", privacy);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent team id for this team
|
||||
*
|
||||
* @param parentTeamId
|
||||
* parentTeamId of team
|
||||
* @return a builder to continue with building
|
||||
*/
|
||||
public GHTeamBuilder parentTeamId(int parentTeamId) {
|
||||
this.builder.with("parent_team_id", parentTeamId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a team with all the parameters.
|
||||
*
|
||||
* @return the gh team
|
||||
* @throws IOException
|
||||
* if team cannot be created
|
||||
*/
|
||||
public GHTeam create() throws IOException {
|
||||
return builder.method("POST").withUrlPath("/orgs/" + orgName + "/teams").fetch(GHTeam.class).wrapUp(root);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.Reader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Array;
|
||||
@@ -38,6 +39,7 @@ import java.lang.reflect.Field;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@@ -61,6 +63,7 @@ import java.util.zip.GZIPInputStream;
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.WillClose;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.logging.Level.*;
|
||||
@@ -74,6 +77,7 @@ import static org.kohsuke.github.GitHub.connect;
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
class Requester {
|
||||
public static final int CONNECTION_ERROR_RETRIES = 2;
|
||||
private final GitHub root;
|
||||
private final List<Entry> args = new ArrayList<Entry>();
|
||||
private final Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
@@ -104,6 +108,11 @@ class Requester {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If timeout issues let's retry after milliseconds.
|
||||
*/
|
||||
private static final int retryTimeoutMillis = 100;
|
||||
|
||||
Requester(GitHub root) {
|
||||
this.root = root;
|
||||
}
|
||||
@@ -470,7 +479,8 @@ class Requester {
|
||||
}
|
||||
|
||||
/**
|
||||
* As stream input stream.
|
||||
* Response input stream. There are scenarios where direct stream reading is needed, however it is better to use
|
||||
* {@link #fetch(Class)} where possible.
|
||||
*
|
||||
* @return the input stream
|
||||
* @throws IOException
|
||||
@@ -491,8 +501,7 @@ class Requester {
|
||||
uc = setupConnection(url);
|
||||
|
||||
try {
|
||||
retryInvalidCached404Response();
|
||||
return supplier.get();
|
||||
return _fetchOrRetry(supplier, CONNECTION_ERROR_RETRIES);
|
||||
} catch (IOException e) {
|
||||
handleApiError(e);
|
||||
} finally {
|
||||
@@ -501,6 +510,86 @@ class Requester {
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T _fetchOrRetry(SupplierThrows<T, IOException> supplier, int retries) throws IOException {
|
||||
int responseCode = -1;
|
||||
String responseMessage = null;
|
||||
// When retries equal 0 the previous call must return or throw, not retry again
|
||||
if (retries < 0) {
|
||||
throw new IllegalArgumentException("'retries' cannot be less than 0");
|
||||
}
|
||||
|
||||
try {
|
||||
// This is where the request is sent and response is processing starts
|
||||
responseCode = uc.getResponseCode();
|
||||
responseMessage = uc.getResponseMessage();
|
||||
|
||||
// If we are caching and get an invalid cached 404, retry it.
|
||||
if (!retryInvalidCached404Response(responseCode, retries)) {
|
||||
return supplier.get();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
// java.net.URLConnection handles 404 exception as FileNotFoundException,
|
||||
// don't wrap exception in HttpException to preserve backward compatibility
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
|
||||
if (!retryConnectionError(e, retries)) {
|
||||
throw new HttpException(responseCode, responseMessage, uc.getURL(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// We did not fetch or throw, retry
|
||||
return _fetchOrRetry(supplier, retries - 1);
|
||||
|
||||
}
|
||||
|
||||
private boolean retryConnectionError(IOException e, int retries) throws IOException {
|
||||
// There are a range of connection errors where we want to wait a moment and just automatically retry
|
||||
boolean connectionError = e instanceof SocketException || e instanceof SocketTimeoutException
|
||||
|| e instanceof SSLHandshakeException;
|
||||
if (connectionError && retries > 0) {
|
||||
LOGGER.log(INFO,
|
||||
e.getMessage() + " while connecting to " + uc.getURL() + ". Sleeping "
|
||||
+ Requester.retryTimeoutMillis + " milliseconds before retrying... ; will try " + retries
|
||||
+ " more time(s)");
|
||||
try {
|
||||
Thread.sleep(Requester.retryTimeoutMillis);
|
||||
} catch (InterruptedException ie) {
|
||||
throw (IOException) new InterruptedIOException().initCause(e);
|
||||
}
|
||||
uc = setupConnection(uc.getURL());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean retryInvalidCached404Response(int responseCode, int retries) throws IOException {
|
||||
// WORKAROUND FOR ISSUE #669:
|
||||
// When the Requester detects a 404 response with an ETag (only happpens when the server's 304
|
||||
// is bogus and would cause cache corruption), try the query again with new request header
|
||||
// that forces the server to not return 304 and return new data instead.
|
||||
//
|
||||
// This solution is transparent to users of this library and automatically handles a
|
||||
// situation that was cause insidious and hard to debug bad responses in caching
|
||||
// scenarios. If GitHub ever fixes their issue and/or begins providing accurate ETags to
|
||||
// their 404 responses, this will result in at worst two requests being made for each 404
|
||||
// responses. However, only the second request will count against rate limit.
|
||||
if (responseCode == 404 && Objects.equals(uc.getRequestMethod(), "GET") && uc.getHeaderField("ETag") != null
|
||||
&& !Objects.equals(uc.getRequestProperty("Cache-Control"), "no-cache") && retries > 0) {
|
||||
LOGGER.log(FINE,
|
||||
"Encountered GitHub invalid cached 404 from " + uc.getURL()
|
||||
+ ". Retrying with \"Cache-Control\"=\"no-cache\"...");
|
||||
|
||||
uc = setupConnection(uc.getURL());
|
||||
// Setting "Cache-Control" to "no-cache" stops the cache from supplying
|
||||
// "If-Modified-Since" or "If-None-Match" values.
|
||||
// This makes GitHub give us current data (not incorrectly cached data)
|
||||
uc.setRequestProperty("Cache-Control", "no-cache");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private <T> T[] concatenatePages(Class<T[]> type, List<T[]> pages, int totalLength) {
|
||||
|
||||
T[] result = type.cast(Array.newInstance(type.getComponentType(), totalLength));
|
||||
@@ -850,10 +939,8 @@ class Requester {
|
||||
private <T> T parse(Class<T> type, T instance, int timeouts) throws IOException {
|
||||
InputStreamReader r = null;
|
||||
int responseCode = -1;
|
||||
String responseMessage = null;
|
||||
try {
|
||||
responseCode = uc.getResponseCode();
|
||||
responseMessage = uc.getResponseMessage();
|
||||
if (responseCode == 304) {
|
||||
return null; // special case handling for 304 unmodified, as the content will be ""
|
||||
}
|
||||
@@ -897,44 +984,11 @@ class Requester {
|
||||
return setResponseHeaders(MAPPER.readerForUpdating(instance).<T>readValue(data));
|
||||
}
|
||||
return null;
|
||||
} catch (FileNotFoundException e) {
|
||||
// java.net.URLConnection handles 404 exception as FileNotFoundException,
|
||||
// don't wrap exception in HttpException to preserve backward compatibility
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
if (e instanceof SocketTimeoutException && timeouts > 0) {
|
||||
LOGGER.log(INFO, "timed out accessing " + uc.getURL() + "; will try " + timeouts + " more time(s)", e);
|
||||
return parse(type, instance, timeouts - 1);
|
||||
}
|
||||
throw new HttpException(responseCode, responseMessage, uc.getURL(), e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(r);
|
||||
}
|
||||
}
|
||||
|
||||
private void retryInvalidCached404Response() throws IOException {
|
||||
// WORKAROUND FOR ISSUE #669:
|
||||
// When the Requester detects a 404 response with an ETag (only happpens when the server's 304
|
||||
// is bogus and would cause cache corruption), try the query again with new request header
|
||||
// that forces the server to not return 304 and return new data instead.
|
||||
//
|
||||
// This solution is transparent to users of this library and automatically handles a
|
||||
// situation that was cause insidious and hard to debug bad responses in caching
|
||||
// scenarios. If GitHub ever fixes their issue and/or begins providing accurate ETags to
|
||||
// their 404 responses, this will result in at worst two requests being made for each 404
|
||||
// responses. However, only the second request will count against rate limit.
|
||||
int responseCode = uc.getResponseCode();
|
||||
if (responseCode == 404 && Objects.equals(uc.getRequestMethod(), "GET") && uc.getHeaderField("ETag") != null
|
||||
&& !Objects.equals(uc.getRequestProperty("Cache-Control"), "no-cache")) {
|
||||
uc = setupConnection(uc.getURL());
|
||||
// Setting "Cache-Control" to "no-cache" stops the cache from supplying
|
||||
// "If-Modified-Since" or "If-None-Match" values.
|
||||
// This makes GitHub give us current data (not incorrectly cached data)
|
||||
uc.setRequestProperty("Cache-Control", "no-cache");
|
||||
uc.getResponseCode();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T setResponseHeaders(T readValue) {
|
||||
if (readValue instanceof GHObject[]) {
|
||||
for (GHObject ghObject : (GHObject[]) readValue) {
|
||||
|
||||
Reference in New Issue
Block a user