blob: 1c4c3db0d38730d758c0f588e89e6ca7745c17f6 [file] [log] [blame]
/*
* Copyright (C) 2014, Google Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.gitrepo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.FileChannel;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.GitCommand;
import org.eclipse.jgit.api.SubmoduleAddCommand;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.gitrepo.internal.RepoText;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FileUtils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* A class used to execute a repo command.
*
* This will parse a repo XML manifest, convert it into .gitmodules file and the
* repository config file.
*
* If called against a bare repository, it will replace all the existing content
* of the repository with the contents populated from the manifest.
*
* repo manifest allows projects overlapping, e.g. one project's path is
* "foo" and another project's path is "foo/bar". This won't
* work in git submodule, so we'll skip all the sub projects
* ("foo/bar" in the example) while converting.
*
* @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
* @since 3.4
*/
public class RepoCommand extends GitCommand<RevCommit> {
private String path;
private String uri;
private String groups;
private String branch;
private PersonIdent author;
private RemoteReader callback;
private InputStream inputStream;
private IncludedFileReader includedReader;
private List<Project> bareProjects;
private Git git;
private ProgressMonitor monitor;
/**
* A callback to get ref sha1 of a repository from its uri.
*
* We provided a default implementation {@link DefaultRemoteReader} to
* use ls-remote command to read the sha1 from the repository and clone the
* repository to read the file. Callers may have their own quicker
* implementation.
*
* @since 3.4
*/
public interface RemoteReader {
/**
* Read a remote ref sha1.
*
* @param uri
* The URI of the remote repository
* @param ref
* The ref (branch/tag/etc.) to read
* @return the sha1 of the remote repository
* @throws GitAPIException
*/
public ObjectId sha1(String uri, String ref) throws GitAPIException;
/**
* Read a file from a remote repository.
*
* @param uri
* The URI of the remote repository
* @param ref
* The ref (branch/tag/etc.) to read
* @param path
* The relative path (inside the repo) to the file to read
* @return the file content.
* @throws GitAPIException
* @throws IOException
* @since 3.5
*/
public byte[] readFile(String uri, String ref, String path)
throws GitAPIException, IOException;
}
/** A default implementation of {@link RemoteReader} callback. */
public static class DefaultRemoteReader implements RemoteReader {
public ObjectId sha1(String uri, String ref) throws GitAPIException {
Map<String, Ref> map = Git
.lsRemoteRepository()
.setRemote(uri)
.callAsMap();
Ref r = RefDatabase.findRef(map, ref);
return r != null ? r.getObjectId() : null;
}
public byte[] readFile(String uri, String ref, String path)
throws GitAPIException, IOException {
File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
Repository repo = Git
.cloneRepository()
.setBare(true)
.setDirectory(dir)
.setURI(uri)
.call()
.getRepository();
try {
return readFileFromRepo(repo, ref, path);
} finally {
FileUtils.delete(dir, FileUtils.RECURSIVE);
}
}
/**
* Read a file from the repository
*
* @param repo
* The repository containing the file
* @param ref
* The ref (branch/tag/etc.) to read
* @param path
* The relative path (inside the repo) to the file to read
* @return the file's content
* @throws GitAPIException
* @throws IOException
* @since 3.5
*/
protected byte[] readFileFromRepo(Repository repo,
String ref, String path) throws GitAPIException, IOException {
ObjectReader reader = repo.newObjectReader();
byte[] result;
try {
ObjectId oid = repo.resolve(ref + ":" + path); //$NON-NLS-1$
result = reader.open(oid).getBytes(Integer.MAX_VALUE);
} finally {
reader.release();
}
return result;
}
}
/**
* A callback to read included xml files.
*
* @since 3.5
*/
public interface IncludedFileReader {
/**
* Read a file from the same base dir of the manifest xml file.
*
* @param path
* The relative path to the file to read
* @return the {@code InputStream} of the file.
* @throws GitAPIException
* @throws IOException
*/
public InputStream readIncludeFile(String path)
throws GitAPIException, IOException;
}
private static class CopyFile {
final Repository repo;
final String path;
final String src;
final String dest;
CopyFile(Repository repo, String path, String src, String dest) {
this.repo = repo;
this.path = path;
this.src = src;
this.dest = dest;
}
void copy() throws IOException {
File srcFile = new File(repo.getWorkTree(),
path + "/" + src); //$NON-NLS-1$
File destFile = new File(repo.getWorkTree(), dest);
FileInputStream input = new FileInputStream(srcFile);
try {
FileOutputStream output = new FileOutputStream(destFile);
try {
FileChannel channel = input.getChannel();
output.getChannel().transferFrom(channel, 0, channel.size());
} finally {
output.close();
}
} finally {
input.close();
}
}
}
private static class Project implements Comparable<Project> {
final String name;
final String path;
final String revision;
final String remote;
final Set<String> groups;
final List<CopyFile> copyfiles;
Project(String name, String path, String revision,
String remote, String groups) {
this.name = name;
if (path != null)
this.path = path;
else
this.path = name;
this.revision = revision;
this.remote = remote;
this.groups = new HashSet<String>();
if (groups != null && groups.length() > 0)
this.groups.addAll(Arrays.asList(groups.split(","))); //$NON-NLS-1$
copyfiles = new ArrayList<CopyFile>();
}
void addCopyFile(CopyFile copyfile) {
copyfiles.add(copyfile);
}
String getPathWithSlash() {
if (path.endsWith("/")) //$NON-NLS-1$
return path;
else
return path + "/"; //$NON-NLS-1$
}
boolean isAncestorOf(Project that) {
return that.getPathWithSlash().startsWith(this.getPathWithSlash());
}
@Override
public boolean equals(Object o) {
if (o instanceof Project) {
Project that = (Project) o;
return this.getPathWithSlash().equals(that.getPathWithSlash());
}
return false;
}
@Override
public int hashCode() {
return this.getPathWithSlash().hashCode();
}
public int compareTo(Project that) {
return this.getPathWithSlash().compareTo(that.getPathWithSlash());
}
}
private static class XmlManifest extends DefaultHandler {
private final RepoCommand command;
private final String filename;
private final String baseUrl;
private final Map<String, String> remotes;
private final Set<String> plusGroups;
private final Set<String> minusGroups;
private List<Project> projects;
private String defaultRemote;
private String defaultRevision;
private IncludedFileReader includedReader;
private int xmlInRead;
private Project currentProject;
XmlManifest(RepoCommand command, IncludedFileReader includedReader,
String filename, String baseUrl, String groups) {
this.command = command;
this.includedReader = includedReader;
this.filename = filename;
// Strip trailing /s to match repo behavior.
int lastIndex = baseUrl.length() - 1;
while (lastIndex >= 0 && baseUrl.charAt(lastIndex) == '/')
lastIndex--;
this.baseUrl = baseUrl.substring(0, lastIndex + 1);
remotes = new HashMap<String, String>();
projects = new ArrayList<Project>();
plusGroups = new HashSet<String>();
minusGroups = new HashSet<String>();
if (groups == null || groups.length() == 0 || groups.equals("default")) { //$NON-NLS-1$
// default means "all,-notdefault"
minusGroups.add("notdefault"); //$NON-NLS-1$
} else {
for (String group : groups.split(",")) { //$NON-NLS-1$
if (group.startsWith("-")) //$NON-NLS-1$
minusGroups.add(group.substring(1));
else
plusGroups.add(group);
}
}
}
void read(InputStream inputStream) throws IOException {
xmlInRead++;
final XMLReader xr;
try {
xr = XMLReaderFactory.createXMLReader();
} catch (SAXException e) {
throw new IOException(JGitText.get().noXMLParserAvailable);
}
xr.setContentHandler(this);
try {
xr.parse(new InputSource(inputStream));
} catch (SAXException e) {
IOException error = new IOException(
RepoText.get().errorParsingManifestFile);
error.initCause(e);
throw error;
}
}
@Override
public void startElement(
String uri,
String localName,
String qName,
Attributes attributes) throws SAXException {
if ("project".equals(qName)) { //$NON-NLS-1$
currentProject = new Project(
attributes.getValue("name"), //$NON-NLS-1$
attributes.getValue("path"), //$NON-NLS-1$
attributes.getValue("revision"), //$NON-NLS-1$
attributes.getValue("remote"), //$NON-NLS-1$
attributes.getValue("groups")); //$NON-NLS-1$
} else if ("remote".equals(qName)) { //$NON-NLS-1$
String alias = attributes.getValue("alias"); //$NON-NLS-1$
String fetch = attributes.getValue("fetch"); //$NON-NLS-1$
remotes.put(attributes.getValue("name"), fetch); //$NON-NLS-1$
if (alias != null)
remotes.put(alias, fetch);
} else if ("default".equals(qName)) { //$NON-NLS-1$
defaultRemote = attributes.getValue("remote"); //$NON-NLS-1$
defaultRevision = attributes.getValue("revision"); //$NON-NLS-1$
if (defaultRevision == null)
defaultRevision = command.branch;
} else if ("copyfile".equals(qName)) { //$NON-NLS-1$
if (currentProject == null)
throw new SAXException(RepoText.get().invalidManifest);
currentProject.addCopyFile(new CopyFile(
command.repo,
currentProject.path,
attributes.getValue("src"), //$NON-NLS-1$
attributes.getValue("dest"))); //$NON-NLS-1$
} else if ("include".equals(qName)) { //$NON_NLS-1$
String name = attributes.getValue("name");
InputStream is = null;
if (includedReader != null) {
try {
is = includedReader.readIncludeFile(name);
} catch (Exception e) {
throw new SAXException(MessageFormat.format(
RepoText.get().errorIncludeFile, name), e);
}
} else if (filename != null) {
int index = filename.lastIndexOf('/');
String path = filename.substring(0, index + 1) + name;
try {
is = new FileInputStream(path);
} catch (IOException e) {
throw new SAXException(MessageFormat.format(
RepoText.get().errorIncludeFile, path), e);
}
}
if (is == null) {
throw new SAXException(
RepoText.get().errorIncludeNotImplemented);
}
try {
read(is);
} catch (IOException e) {
throw new SAXException(e);
}
}
}
@Override
public void endElement(
String uri,
String localName,
String qName) throws SAXException {
if ("project".equals(qName)) { //$NON-NLS-1$
projects.add(currentProject);
currentProject = null;
}
}
@Override
public void endDocument() throws SAXException {
xmlInRead--;
if (xmlInRead != 0)
return;
// Only do the following after we finished reading everything.
removeNotInGroup();
removeOverlaps();
Map<String, String> remoteUrls = new HashMap<String, String>();
URI baseUri;
try {
baseUri = new URI(baseUrl);
} catch (URISyntaxException e) {
throw new SAXException(e);
}
for (Project proj : projects) {
String remote = proj.remote;
if (remote == null) {
if (defaultRemote == null) {
if (filename != null)
throw new SAXException(MessageFormat.format(
RepoText.get().errorNoDefaultFilename,
filename));
else
throw new SAXException(
RepoText.get().errorNoDefault);
}
remote = defaultRemote;
}
String remoteUrl = remoteUrls.get(remote);
if (remoteUrl == null) {
remoteUrl = baseUri.resolve(remotes.get(remote)).toString();
if (!remoteUrl.endsWith("/"))
remoteUrl = remoteUrl + "/";
remoteUrls.put(remote, remoteUrl);
}
command.addSubmodule(remoteUrl + proj.name,
proj.path,
proj.revision == null
? defaultRevision : proj.revision,
proj.copyfiles);
}
}
/** Remove projects that are not in our desired groups. */
void removeNotInGroup() {
Iterator<Project> iter = projects.iterator();
while (iter.hasNext())
if (!inGroups(iter.next()))
iter.remove();
}
/** Remove projects that sits in a subdirectory of any other project. */
void removeOverlaps() {
Collections.sort(projects);
Iterator<Project> iter = projects.iterator();
if (!iter.hasNext())
return;
Project last = iter.next();
while (iter.hasNext()) {
Project p = iter.next();
if (last.isAncestorOf(p))
iter.remove();
else
last = p;
}
}
boolean inGroups(Project proj) {
for (String group : minusGroups) {
if (proj.groups.contains(group)) {
// minus groups have highest priority.
return false;
}
}
if (plusGroups.isEmpty() || plusGroups.contains("all")) { //$NON-NLS-1$
// empty plus groups means "all"
return true;
}
for (String group : plusGroups) {
if (proj.groups.contains(group))
return true;
}
return false;
}
}
@SuppressWarnings("serial")
private static class ManifestErrorException extends GitAPIException {
ManifestErrorException(Throwable cause) {
super(RepoText.get().invalidManifest, cause);
}
}
@SuppressWarnings("serial")
private static class RemoteUnavailableException extends GitAPIException {
RemoteUnavailableException(String uri) {
super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
}
}
/**
* @param repo
*/
public RepoCommand(final Repository repo) {
super(repo);
}
/**
* Set path to the manifest XML file.
*
* Calling {@link #setInputStream} will ignore the path set here.
*
* @param path
* (with <code>/</code> as separator)
* @return this command
*/
public RepoCommand setPath(final String path) {
this.path = path;
return this;
}
/**
* Set the input stream to the manifest XML.
*
* Setting inputStream will ignore the path set. It will be closed in
* {@link #call}.
*
* @param inputStream
* @return this command
* @since 3.5
*/
public RepoCommand setInputStream(final InputStream inputStream) {
this.inputStream = inputStream;
return this;
}
/**
* Set base URI of the pathes inside the XML
*
* @param uri
* @return this command
*/
public RepoCommand setURI(final String uri) {
this.uri = uri;
return this;
}
/**
* Set groups to sync
*
* @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
* @return this command
*/
public RepoCommand setGroups(final String groups) {
this.groups = groups;
return this;
}
/**
* Set default branch.
*
* This is generally the name of the branch the manifest file was in. If
* there's no default revision (branch) specified in manifest and no
* revision specified in project, this branch will be used.
*
* @param branch
* @return this command
*/
public RepoCommand setBranch(final String branch) {
this.branch = branch;
return this;
}
/**
* The progress monitor associated with the clone operation. By default,
* this is set to <code>NullProgressMonitor</code>
*
* @see org.eclipse.jgit.lib.NullProgressMonitor
* @param monitor
* @return this command
*/
public RepoCommand setProgressMonitor(final ProgressMonitor monitor) {
this.monitor = monitor;
return this;
}
/**
* Set the author/committer for the bare repository commit.
*
* For non-bare repositories, the current user will be used and this will be
* ignored.
*
* @param author
* @return this command
*/
public RepoCommand setAuthor(final PersonIdent author) {
this.author = author;
return this;
}
/**
* Set the GetHeadFromUri callback.
*
* This is only used in bare repositories.
*
* @param callback
* @return this command
*/
public RepoCommand setRemoteReader(final RemoteReader callback) {
this.callback = callback;
return this;
}
/**
* Set the IncludedFileReader callback.
*
* @param reader
* @return this command
* @since 3.5
*/
public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
this.includedReader = reader;
return this;
}
@Override
public RevCommit call() throws GitAPIException {
try {
checkCallable();
if (uri == null || uri.length() == 0)
throw new IllegalArgumentException(
JGitText.get().uriNotConfigured);
if (inputStream == null) {
if (path == null || path.length() == 0)
throw new IllegalArgumentException(
JGitText.get().pathNotConfigured);
try {
inputStream = new FileInputStream(path);
} catch (IOException e) {
throw new IllegalArgumentException(
JGitText.get().pathNotConfigured);
}
}
if (repo.isBare()) {
bareProjects = new ArrayList<Project>();
if (author == null)
author = new PersonIdent(repo);
if (callback == null)
callback = new DefaultRemoteReader();
} else
git = new Git(repo);
XmlManifest manifest = new XmlManifest(
this, includedReader, path, uri, groups);
try {
manifest.read(inputStream);
} catch (IOException e) {
throw new ManifestErrorException(e);
}
} finally {
try {
if (inputStream != null)
inputStream.close();
} catch (IOException e) {
// Just ignore it, it's not important.
}
}
if (repo.isBare()) {
DirCache index = DirCache.newInCore();
DirCacheBuilder builder = index.builder();
ObjectInserter inserter = repo.newObjectInserter();
RevWalk rw = new RevWalk(repo);
try {
Config cfg = new Config();
for (Project proj : bareProjects) {
String name = proj.path;
String nameUri = proj.name;
cfg.setString("submodule", name, "path", name); //$NON-NLS-1$ //$NON-NLS-2$
cfg.setString("submodule", name, "url", nameUri); //$NON-NLS-1$ //$NON-NLS-2$
// create gitlink
DirCacheEntry dcEntry = new DirCacheEntry(name);
ObjectId objectId;
if (ObjectId.isId(proj.revision))
objectId = ObjectId.fromString(proj.revision);
else {
objectId = callback.sha1(nameUri, proj.revision);
}
if (objectId == null)
throw new RemoteUnavailableException(nameUri);
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.GITLINK);
builder.add(dcEntry);
for (CopyFile copyfile : proj.copyfiles) {
byte[] src = callback.readFile(
nameUri, proj.revision, copyfile.src);
objectId = inserter.insert(Constants.OBJ_BLOB, src);
dcEntry = new DirCacheEntry(copyfile.dest);
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.REGULAR_FILE);
builder.add(dcEntry);
}
}
String content = cfg.toText();
// create a new DirCacheEntry for .gitmodules file.
final DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
content.getBytes(Constants.CHARACTER_ENCODING));
dcEntry.setObjectId(objectId);
dcEntry.setFileMode(FileMode.REGULAR_FILE);
builder.add(dcEntry);
builder.finish();
ObjectId treeId = index.writeTree(inserter);
// Create a Commit object, populate it and write it
ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$
CommitBuilder commit = new CommitBuilder();
commit.setTreeId(treeId);
if (headId != null)
commit.setParentIds(headId);
commit.setAuthor(author);
commit.setCommitter(author);
commit.setMessage(RepoText.get().repoCommitMessage);
ObjectId commitId = inserter.insert(commit);
inserter.flush();
RefUpdate ru = repo.updateRef(Constants.HEAD);
ru.setNewObjectId(commitId);
ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
Result rc = ru.update(rw);
switch (rc) {
case NEW:
case FORCED:
case FAST_FORWARD:
// Successful. Do nothing.
break;
case REJECTED:
case LOCK_FAILURE:
throw new ConcurrentRefUpdateException(
JGitText.get().couldNotLockHEAD, ru.getRef(),
rc);
default:
throw new JGitInternalException(MessageFormat.format(
JGitText.get().updatingRefFailed,
Constants.HEAD, commitId.name(), rc));
}
return rw.parseCommit(commitId);
} catch (IOException e) {
throw new ManifestErrorException(e);
} finally {
rw.release();
}
} else {
return git
.commit()
.setMessage(RepoText.get().repoCommitMessage)
.call();
}
}
private void addSubmodule(String url, String name, String revision,
List<CopyFile> copyfiles) throws SAXException {
if (repo.isBare()) {
Project proj = new Project(url, name, revision, null, null);
proj.copyfiles.addAll(copyfiles);
bareProjects.add(proj);
} else {
SubmoduleAddCommand add = git
.submoduleAdd()
.setPath(name)
.setURI(url);
if (monitor != null)
add.setProgressMonitor(monitor);
try {
Repository subRepo = add.call();
if (revision != null) {
Git sub = new Git(subRepo);
sub.checkout().setName(findRef(revision, subRepo)).call();
git.add().addFilepattern(name).call();
}
for (CopyFile copyfile : copyfiles) {
copyfile.copy();
git.add().addFilepattern(copyfile.dest).call();
}
} catch (GitAPIException e) {
throw new SAXException(e);
} catch (IOException e) {
throw new SAXException(e);
}
}
}
private static String findRef(String ref, Repository repo)
throws IOException {
if (!ObjectId.isId(ref)) {
Ref r = repo.getRef(Constants.DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$
if (r != null)
return r.getName();
}
return ref;
}
}