Compare commits

..

25 Commits

Author SHA1 Message Date
Kohsuke Kawaguchi
9e62776905 [maven-release-plugin] prepare release github-api-1.25 2012-05-21 22:42:19 -07:00
Kohsuke Kawaguchi
9ba74b945d added permission check methods 2012-05-21 22:40:43 -07:00
Kohsuke Kawaguchi
66656ce612 when authenticated, repository returns additional information, so always send in a credential when one is available 2012-05-21 22:37:48 -07:00
Kohsuke Kawaguchi
73a20ad829 [maven-release-plugin] prepare for next development iteration 2012-05-21 22:16:52 -07:00
Kohsuke Kawaguchi
6fc9a546cb [maven-release-plugin] prepare release github-api-1.24 2012-05-21 22:16:47 -07:00
Kohsuke Kawaguchi
3f6c225948 updated URL 2012-05-21 22:16:09 -07:00
Kohsuke Kawaguchi
c7e9650a39 added code to list up public keys 2012-05-21 22:15:34 -07:00
Kohsuke Kawaguchi
fd28a36b74 [maven-release-plugin] prepare for next development iteration 2012-04-24 17:22:19 -07:00
Kohsuke Kawaguchi
86543c84db [maven-release-plugin] prepare release github-api-1.23 2012-04-24 17:22:14 -07:00
Kohsuke Kawaguchi
706cdac4cc bug fix 2012-04-24 17:21:33 -07:00
Kohsuke Kawaguchi
0b69743792 added comment deletion 2012-04-24 17:17:35 -07:00
Kohsuke Kawaguchi
dd54a00172 added update 2012-04-24 17:13:48 -07:00
Kohsuke Kawaguchi
b5514b891a added a method to create a new commit comment 2012-04-24 17:06:23 -07:00
Kohsuke Kawaguchi
31a6eca97f added commit comment support 2012-04-24 16:54:05 -07:00
Kohsuke Kawaguchi
8557676561 making names consistent with GHBranch 2012-04-24 16:36:18 -07:00
Kohsuke Kawaguchi
39631461ae added list commits 2012-04-24 16:30:01 -07:00
Kohsuke Kawaguchi
a124d7f714 added some tests 2012-04-24 16:16:13 -07:00
Kohsuke Kawaguchi
fff07bf70b fixed a bug in error handling 2012-04-24 16:14:13 -07:00
Kohsuke Kawaguchi
53d09bb5d8 added object representation for GitHub commit 2012-04-24 16:11:55 -07:00
Kohsuke Kawaguchi
a8ecf0bef0 [maven-release-plugin] prepare for next development iteration 2012-04-12 11:36:57 -07:00
Kohsuke Kawaguchi
74b3902d5f [maven-release-plugin] prepare release github-api-1.22 2012-04-12 11:36:49 -07:00
Kohsuke Kawaguchi
7433ed968e monkey test code 2012-04-12 11:05:18 -07:00
Kohsuke Kawaguchi
ddf2d69a68 this method can return null 2012-04-12 11:05:04 -07:00
Kohsuke Kawaguchi
f5b34861bd avoid NPE 2012-04-12 11:01:43 -07:00
Kohsuke Kawaguchi
f0ff31a1af [maven-release-plugin] prepare for next development iteration 2012-04-11 18:16:16 -07:00
14 changed files with 676 additions and 17 deletions

2
README
View File

@@ -1,3 +1,3 @@
Java API for GitHub
See http://kohsuke.org/github-api/ for more details
See http://github-api.kohsuke.org/ for more details

View File

@@ -7,7 +7,7 @@
</parent>
<artifactId>github-api</artifactId>
<version>1.21</version>
<version>1.25</version>
<name>GitHub API for Java</name>
<url>http://github-api.kohsuke.org/</url>
<description>GitHub API for Java</description>

View File

@@ -0,0 +1,243 @@
package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.kohsuke.github.ApiVersion.V3;
/**
* A commit in a repository.
*
* @author Kohsuke Kawaguchi
* @see GHRepository#getCommit(String)
* @see GHCommitComment#getCommit()
*/
public class GHCommit {
private GHRepository owner;
public static class Stats {
int total,additions,deletions;
}
/**
* A file that was modified.
*/
public static class File {
String status;
int changes,additions,deletions;
String raw_url, blob_url, filename, sha, patch;
/**
* Number of lines added + removed.
*/
public int getLinesChanged() {
return changes;
}
/**
* Number of lines added.
*/
public int getLinesAdded() {
return additions;
}
/**
* Number of lines removed.
*/
public int getLinesDeleted() {
return deletions;
}
/**
* "modified", "added", or "deleted"
*/
public String getStatus() {
return status;
}
/**
* Just the base name and the extension without any directory name.
*/
public String getFileName() {
return filename;
}
/**
* The actual change.
*/
public String getPatch() {
return patch;
}
/**
* URL like 'https://raw.github.com/jenkinsci/jenkins/4eb17c197dfdcf8ef7ff87eb160f24f6a20b7f0e/core/pom.xml'
* that resolves to the actual content of the file.
*/
public URL getRawUrl() {
return GitHub.parseURL(raw_url);
}
/**
* URL like 'https://github.com/jenkinsci/jenkins/blob/1182e2ebb1734d0653142bd422ad33c21437f7cf/core/pom.xml'
* that resolves to the HTML page that describes this file.
*/
public URL getBlobUrl() {
return GitHub.parseURL(blob_url);
}
/**
* [0-9a-f]{40} SHA1 checksum.
*/
public String getSha() {
return sha;
}
}
public static class Parent {
String url,sha;
}
static class User {
// TODO: what if someone who doesn't have an account on GitHub makes a commit?
String url,avatar_url,login,gravatar_id;
int id;
}
String url,sha;
List<File> files;
Stats stats;
List<Parent> parents;
User author,committer;
/**
* The repository that contains the commit.
*/
public GHRepository getOwner() {
return owner;
}
/**
* Number of lines added + removed.
*/
public int getLinesChanged() {
return stats.total;
}
/**
* Number of lines added.
*/
public int getLinesAdded() {
return stats.additions;
}
/**
* Number of lines removed.
*/
public int getLinesDeleted() {
return stats.deletions;
}
/**
* [0-9a-f]{40} SHA1 checksum.
*/
public String getSHA1() {
return sha;
}
/**
* List of files changed/added/removed in this commit.
*
* @return
* Can be empty but never null.
*/
public List<File> getFiles() {
return files!=null ? Collections.unmodifiableList(files) : Collections.<File>emptyList();
}
/**
* Returns the SHA1 of parent commit objects.
*/
public List<String> getParentSHA1s() {
if (parents==null) return Collections.emptyList();
return new AbstractList<String>() {
@Override
public String get(int index) {
return parents.get(index).sha;
}
@Override
public int size() {
return parents.size();
}
};
}
/**
* Resolves the parent commit objects and return them.
*/
public List<GHCommit> getParents() throws IOException {
List<GHCommit> r = new ArrayList<GHCommit>();
for (String sha1 : getParentSHA1s())
r.add(owner.getCommit(sha1));
return r;
}
public GHUser getAuthor() throws IOException {
return resolveUser(author);
}
public GHUser getCommitter() throws IOException {
return resolveUser(committer);
}
private GHUser resolveUser(User author) throws IOException {
if (author==null || author.login==null) return null;
return owner.root.getUser(author.login);
}
/**
* Lists up all the commit comments in this repository.
*/
public PagedIterable<GHCommitComment> listComments() {
return new PagedIterable<GHCommitComment>() {
public PagedIterator<GHCommitComment> iterator() {
return new PagedIterator<GHCommitComment>(owner.root.retrievePaged(String.format("/repos/%s/%s/commits/%s/comments",owner.getOwnerName(),owner.getName(),sha),GHCommitComment[].class,false,V3)) {
@Override
protected void wrapUp(GHCommitComment[] page) {
for (GHCommitComment c : page)
c.wrap(owner);
}
};
}
};
}
/**
* Creates a commit comment.
*
* I'm not sure how path/line/position parameters interact with each other.
*/
public GHCommitComment createComment(String body, String path, Integer line, Integer position) throws IOException {
GHCommitComment r = new Poster(owner.root,V3)
.with("body",body)
.with("path",path)
.with("line",line)
.with("position",position)
.withCredential()
.to(String.format("/repos/%s/%s/commits/%s/comments",owner.getOwnerName(),owner.getName(),sha),GHCommitComment.class);
return r.wrap(owner);
}
public GHCommitComment createComment(String body) throws IOException {
return createComment(body,null,null,null);
}
GHCommit wrapUp(GHRepository owner) {
this.owner = owner;
return this;
}
}

View File

@@ -0,0 +1,125 @@
package org.kohsuke.github;
import java.io.IOException;
import java.net.URL;
import java.util.Date;
import static org.kohsuke.github.ApiVersion.V3;
/**
* A comment attached to a commit (or a specific line in a specific file of a commit.)
*
* @author Kohsuke Kawaguchi
* @see GHRepository#listCommitComments()
* @see GHCommit#listComments()
* @see GHCommit#createComment(String, String, Integer, Integer)
*/
public class GHCommitComment {
private GHRepository owner;
String updated_at, created_at;
String body, url, html_url, commit_id;
Integer line;
int id;
String path;
User user;
static class User {
// TODO: what if someone who doesn't have an account on GitHub makes a commit?
String url,avatar_url,login,gravatar_id;
int id;
}
public GHRepository getOwner() {
return owner;
}
public Date getCreatedAt() {
return GitHub.parseDate(created_at);
}
public Date getUpdatedAt() {
return GitHub.parseDate(updated_at);
}
/**
* URL like 'https://github.com/kohsuke/sandbox-ant/commit/8ae38db0ea5837313ab5f39d43a6f73de3bd9000#commitcomment-1252827' to
* show this commit comment in a browser.
*/
public URL getHtmlUrl() {
return GitHub.parseURL(html_url);
}
public String getSHA1() {
return commit_id;
}
/**
* Commit comment in the GitHub flavored markdown format.
*/
public String getBody() {
return body;
}
/**
* A commit comment can be on a specific line of a specific file, if so, this field points to a file.
* Otherwise null.
*/
public String getPath() {
return path;
}
/**
* A commit comment can be on a specific line of a specific file, if so, this field points to the line number in the file.
* Otherwise -1.
*/
public int getLine() {
return line!=null ? line : -1;
}
public int getId() {
return id;
}
/**
* Gets the user who put this comment.
*/
public GHUser getUser() throws IOException {
return owner.root.getUser(user.login);
}
/**
* Gets the commit to which this comment is associated with.
*/
public GHCommit getCommit() throws IOException {
return getOwner().getCommit(getSHA1());
}
/**
* Updates the body of the commit message.
*/
public void update(String body) throws IOException {
GHCommitComment r = new Poster(owner.root,V3)
.with("body",body)
.withCredential()
.to(getApiTail(),GHCommitComment.class,"PATCH");
this.body = body;
}
/**
* Deletes this comment.
*/
public void delete() throws IOException {
new Poster(owner.root,V3).withCredential().to(getApiTail(),null,"DELETE");
}
private String getApiTail() {
return String.format("/repos/%s/%s/comments/%s",owner.getOwnerName(),owner.getName(),id);
}
GHCommitComment wrap(GHRepository owner) {
this.owner = owner;
return this;
}
}

View File

@@ -0,0 +1,48 @@
package org.kohsuke.github;
import org.apache.commons.lang.builder.ToStringBuilder;
/**
* SSH public key.
*
* @author Kohsuke Kawaguchi
*/
public class GHKey {
/*package almost final*/ GitHub root;
private String url, key, title;
private boolean verified;
private int id;
public int getId() {
return id;
}
public String getKey() {
return key;
}
public String getTitle() {
return title;
}
/**
* Something like "https://api.github.com/user/keys/73593"
*/
public String getUrl() {
return url;
}
public boolean isVerified() {
return verified;
}
/*package*/ GHKey wrap(GitHub root) {
this.root = root;
return this;
}
public String toString() {
return new ToStringBuilder(this).append("title",title).append("id",id).append("key",key).toString();
}
}

View File

@@ -26,6 +26,16 @@ public class GHMyself extends GHUser {
String[] addresses = root.retrieveWithAuth3("/user/emails",String[].class);
return Collections.unmodifiableList(Arrays.asList(addresses));
}
/**
* Returns the read-only list of all the pulic keys of the current user.
*
* @return
* Always non-null.
*/
public List<GHKey> getPublicKeys() throws IOException {
return Collections.unmodifiableList(Arrays.asList(root.retrieveWithAuth3("/user/keys",GHKey[].class)));
}
// public void addEmails(Collection<String> emails) throws IOException {
//// new Poster(root,ApiVersion.V3).withCredential().to("/user/emails");

View File

@@ -43,15 +43,6 @@ public class GHOrganization extends GHPerson {
return root.retrieveWithAuth("/organizations/"+login+"/teams",JsonTeams.class).toMap(this);
}
@Override
public GHRepository getRepository(String name) throws IOException {
try {
return root.retrieveWithAuth3("/repos/" + login + '/' + name, GHRepository.class).wrap(root);
} catch (FileNotFoundException e) {
return null;
}
}
/**
* Publicizes the membership.
*/

View File

@@ -1,5 +1,7 @@
package org.kohsuke.github;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Arrays;
@@ -85,7 +87,7 @@ public abstract class GHPerson {
*/
public GHRepository getRepository(String name) throws IOException {
try {
return root.retrieve3("/repos/" + login + '/' + name, GHRepository.class).wrap(root);
return root.retrieveWithAuth3("/repos/" + login + '/' + name, GHRepository.class).wrap(root);
} catch (FileNotFoundException e) {
return null;
}

View File

@@ -72,7 +72,15 @@ public class GHRepository {
private String created_at, pushed_at;
private Map<Integer,GHMilestone> milestones = new HashMap<Integer, GHMilestone>();
private String master_branch;
private String master_branch,language;
private Map<String,GHCommit> commits = new HashMap<String, GHCommit>();
private GHRepoPermission permissions;
private static class GHRepoPermission {
boolean pull,push,admin;
}
public String getDescription() {
return description;
@@ -112,6 +120,25 @@ public class GHRepository {
return name;
}
public boolean hasPullAccess() {
return permissions!=null && permissions.pull;
}
public boolean hasPushAccess() {
return permissions!=null && permissions.push;
}
public boolean hasAdminAccess() {
return permissions!=null && permissions.admin;
}
/**
* Gets the primary programming language.
*/
public String getLanguage() {
return language;
}
public GHUser getOwner() throws IOException {
return root.getUser(owner.login); // because 'owner' isn't fully populated
}
@@ -156,6 +183,11 @@ public class GHRepository {
return open_issues;
}
/**
*
* @return
* null if the repository was never pushed at.
*/
public Date getPushedAt() {
return GitHub.parseDate(pushed_at);
}
@@ -266,7 +298,7 @@ public class GHRepository {
String url = "/repos/delete/" + owner.login +"/"+name;
DeleteToken token = poster.to(url, DeleteToken.class);
poster.with("delete_token",token.delete_token).to(url);
poster.with("delete_token", token.delete_token).to(url);
}
/**
@@ -355,6 +387,51 @@ public class GHRepository {
return root.retrieveWithAuth3(String.format("/repos/%s/%s/hooks/%d",owner.login,name,id),GHHook.class).wrap(this);
}
/**
* Gets a commit object in this repository.
*/
public GHCommit getCommit(String sha1) throws IOException {
GHCommit c = commits.get(sha1);
if (c==null) {
c = root.retrieve3(String.format("/repos/%s/%s/commits/%s",owner.login,name,sha1),GHCommit.class).wrapUp(this);
commits.put(sha1,c);
}
return c;
}
/**
* Lists all the commits.
*/
public PagedIterable<GHCommit> listCommits() {
return new PagedIterable<GHCommit>() {
public PagedIterator<GHCommit> iterator() {
return new PagedIterator<GHCommit>(root.retrievePaged(String.format("/repos/%s/%s/commits",owner.login,name),GHCommit[].class,false,V3)) {
protected void wrapUp(GHCommit[] page) {
for (GHCommit c : page)
c.wrapUp(GHRepository.this);
}
};
}
};
}
/**
* Lists up all the commit comments in this repository.
*/
public PagedIterable<GHCommitComment> listCommitComments() {
return new PagedIterable<GHCommitComment>() {
public PagedIterator<GHCommitComment> iterator() {
return new PagedIterator<GHCommitComment>(root.retrievePaged(String.format("/repos/%s/%s/comments",owner.login,name),GHCommitComment[].class,false,V3)) {
@Override
protected void wrapUp(GHCommitComment[] page) {
for (GHCommitComment c : page)
c.wrap(GHRepository.this);
}
};
}
};
}
/**
*
* See https://api.github.com/hooks for possible names and their configuration scheme.

View File

@@ -320,7 +320,7 @@ public class GitHub {
*/
private InputStream wrapStream(HttpURLConnection uc, InputStream in) throws IOException {
String encoding = uc.getContentEncoding();
if (encoding==null) return in;
if (encoding==null || in==null) return in;
if (encoding.equals("gzip")) return new GZIPInputStream(in);
throw new UnsupportedOperationException("Unexpected Content-Encoding: "+encoding);
@@ -489,6 +489,7 @@ public class GitHub {
}
/*package*/ static Date parseDate(String timestamp) {
if (timestamp==null) return null;
for (String f : TIME_FORMATS) {
try {
SimpleDateFormat df = new SimpleDateFormat(f);

View File

@@ -0,0 +1,8 @@
package org.kohsuke.github;
/**
* @author Kohsuke Kawaguchi
*/
public interface PagedIterable<T> extends Iterable<T> {
PagedIterator<T> iterator();
}

View File

@@ -0,0 +1,64 @@
package org.kohsuke.github;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* Iterator over a pagenated data source.
*
* Aside from the normal iterator operation, this method exposes {@link #nextPage()}
* that allows the caller to retrieve items per page.
*
* @author Kohsuke Kawaguchi
*/
public abstract class PagedIterator<T> implements Iterator<T> {
private final Iterator<T[]> base;
/**
* Current batch that we retrieved but haven't returned to the caller.
*/
private T[] current;
private int pos;
/*package*/ PagedIterator(Iterator<T[]> base) {
this.base = base;
}
protected abstract void wrapUp(T[] page);
public boolean hasNext() {
return (current!=null && pos<current.length) || base.hasNext();
}
public T next() {
fetch();
return current[pos++];
}
private void fetch() {
while (current==null || current.length<=pos) {
current = base.next();
wrapUp(current);
pos = 0;
}
// invariant at the end: there's some data to retrieve
}
public void remove() {
throw new UnsupportedOperationException();
}
/**
* Gets the next page worth of data.
*/
public List<T> nextPage() {
fetch();
List<T> r = Arrays.asList(current);
r = r.subList(pos,r.size());
current = null;
pos = 0;
return r;
}
}

View File

@@ -33,7 +33,9 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
@@ -86,6 +88,12 @@ class Poster {
return _with(key, value);
}
public Poster with(String key, Integer value) {
if (value!=null)
_with(key, value.intValue());
return this;
}
public Poster with(String key, boolean value) {
return _with(key, value);
}
@@ -136,7 +144,18 @@ class Poster {
uc.setRequestProperty("Authorization", "Basic " + root.encodedAuthorization);
}
}
uc.setRequestMethod(method);
try {
uc.setRequestMethod(method);
} catch (ProtocolException e) {
// JDK only allows one of the fixed set of verbs. Try to override that
try {
Field $method = HttpURLConnection.class.getDeclaredField("method");
$method.setAccessible(true);
$method.set(uc,method);
} catch (Exception x) {
throw (IOException)new IOException("Failed to set the custom verb").initCause(x);
}
}
if (v==ApiVersion.V2) {

View File

@@ -1,12 +1,16 @@
package org.kohsuke;
import junit.framework.TestCase;
import org.kohsuke.github.GHCommit;
import org.kohsuke.github.GHCommit.File;
import org.kohsuke.github.GHCommitComment;
import org.kohsuke.github.GHEvent;
import org.kohsuke.github.GHEventInfo;
import org.kohsuke.github.GHEventPayload;
import org.kohsuke.github.GHHook;
import org.kohsuke.github.GHBranch;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHKey;
import org.kohsuke.github.GHMyself;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHOrganization.Permission;
@@ -14,9 +18,11 @@ import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHTeam;
import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.PagedIterable;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.List;
@@ -37,6 +43,15 @@ public class AppTest extends TestCase {
r.getPullRequest(1);
r.getPullRequests(GHIssueState.OPEN);
}
public void testRepoPermissions() throws Exception {
GitHub gh = GitHub.connect();
GHRepository r = gh.getOrganization("jenkinsci").getRepository("jenkins");
assertTrue(r.hasPullAccess());
r = gh.getOrganization("github").getRepository("tire");
assertFalse(r.hasAdminAccess());
}
public void tryGetMyself() throws Exception {
GitHub hub = GitHub.connect();
@@ -44,6 +59,17 @@ public class AppTest extends TestCase {
System.out.println(me);
GHUser u = hub.getUser("kohsuke2");
System.out.println(u);
for (List<GHRepository> lst : me.iterateRepositories(100)) {
for (GHRepository r : lst) {
System.out.println(r.getPushedAt());
}
}
}
public void testPublicKeys() throws Exception {
GitHub gh = GitHub.connect();
List<GHKey> keys = gh.getMyself().getPublicKeys();
System.out.println(keys);
}
public void tryOrgFork() throws Exception {
@@ -69,13 +95,58 @@ public class AppTest extends TestCase {
System.out.println(o);
}
public void testCommit() throws Exception {
GitHub gitHub = GitHub.connect();
GHCommit commit = gitHub.getUser("jenkinsci").getRepository("jenkins").getCommit("08c1c9970af4d609ae754fbe803e06186e3206f7");
System.out.println(commit);
assertEquals(1, commit.getParents().size());
assertEquals(1,commit.getFiles().size());
File f = commit.getFiles().get(0);
assertEquals(48,f.getLinesChanged());
assertEquals("modified",f.getStatus());
assertEquals("changelog.html",f.getFileName());
}
public void testListCommits() throws Exception {
GitHub gitHub = GitHub.connect();
List<String> sha1 = new ArrayList<String>();
for (GHCommit c : gitHub.getUser("kohsuke").getRepository("empty-commit").listCommits()) {
System.out.println(c.getSHA1());
sha1.add(c.getSHA1());
}
assertEquals("fdfad6be4db6f96faea1f153fb447b479a7a9cb7",sha1.get(0));
assertEquals(1,sha1.size());
}
public void testBranches() throws Exception {
GitHub gitHub = GitHub.connect();
Map<String,GHBranch> b =
gitHub.getUser("jenkinsci").getRepository("jenkins").getBranches();
System.out.println(b);
}
public void testCommitComment() throws Exception {
GitHub gitHub = GitHub.connect();
GHRepository r = gitHub.getUser("jenkinsci").getRepository("jenkins");
PagedIterable<GHCommitComment> comments = r.listCommitComments();
List<GHCommitComment> batch = comments.iterator().nextPage();
for (GHCommitComment comment : batch) {
System.out.println(comment.getBody());
assertSame(comment.getOwner(), r);
}
}
public void tryCreateCommitComment() throws Exception {
GitHub gitHub = GitHub.connect();
GHCommit commit = gitHub.getUser("kohsuke").getRepository("sandbox-ant").getCommit("8ae38db0ea5837313ab5f39d43a6f73de3bd9000");
GHCommitComment c = commit.createComment("[testing](http://kohsuse.org/)");
System.out.println(c);
c.update("updated text");
System.out.println(c);
c.delete();
}
public void tryHook() throws Exception {
GitHub gitHub = GitHub.connect();
GHRepository r = gitHub.getMyself().getRepository("test2");