blob: 280d54ace66709ff3b3a622b8e763268b473a4c2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.hudson.git;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.ini4j.Ini;
import org.ini4j.InvalidFileFormatException;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.modules.hudson.api.ConnectionBuilder;
import org.netbeans.modules.hudson.api.HudsonJob;
import org.netbeans.modules.hudson.api.HudsonJobBuild;
import org.netbeans.modules.hudson.api.Utilities;
import static org.netbeans.modules.hudson.git.Bundle.*;
import org.netbeans.modules.hudson.ui.api.HudsonSCMHelper;
import org.netbeans.modules.hudson.spi.HudsonJobChangeItem;
import org.netbeans.modules.hudson.spi.HudsonSCM;
import org.netbeans.modules.hudson.spi.HudsonSCM.ConfigurationStatus;
import org.openide.util.NbBundle.Messages;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.OutputListener;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@ServiceProvider(service=HudsonSCM.class, position=300)
public class HudsonGitSCM implements HudsonSCM {
private static final Logger LOG = Logger.getLogger(HudsonGitSCM.class.getName());
@Messages({
"# {0} - original URL", "# {1} - replacement URL", "ro_replacement=Replacing {0} with {1} in Hudson configuration.",
"# {0} - repository location", "warning.local_repo={0} will only be accessible from a Hudson server on the same machine."
})
@Override public Configuration forFolder(File folder) {
if (!new File(folder, ".git").isDirectory()) {
return null;
}
final URI origin = getRemoteOrigin(org.openide.util.Utilities.toURI(folder), null);
final String replacement = origin != null ? roReplacement(origin.toString()) : folder.getAbsolutePath();
return new Configuration() {
@Override public void configure(Document doc) {
Element root = doc.getDocumentElement();
Element configXmlSCM = (Element) root.appendChild(doc.createElement("scm"));
configXmlSCM.setAttribute("class", "hudson.plugins.git.GitSCM");
// GitSCM config is horribly complex. Let readResolve do the hard work for now.
// Note that all this will be wrong if the local repo is using a nondefault remote, or a branch, etc. etc.
configXmlSCM.appendChild(doc.createElement("source")).appendChild(doc.createTextNode(replacement != null ? replacement : origin.toString()));
// XXX consider <clean>true</clean> (though it does not seem to work)
HudsonSCMHelper.addTrigger(doc);
}
@Override public ConfigurationStatus problems() {
if (origin == null) {
return ConfigurationStatus.withWarning(warning_local_repo(replacement));
} else if (replacement != null) {
return ConfigurationStatus.withWarning(ro_replacement(origin, replacement));
} else {
return null;
}
}
};
}
@Override public String translateWorkspacePath(HudsonJob job, String workspacePath, File localRoot) {
return null; // XXX
}
@Override public List<? extends HudsonJobChangeItem> parseChangeSet(HudsonJobBuild build) {
final Element changeSet;
try {
changeSet = XMLUtil.findElement(new ConnectionBuilder().job(build.getJob()).url(build.getUrl() + "api/xml?tree=changeSet[kind,items[id,author[fullName],msg,paths[file,editType]]]").parseXML().getDocumentElement(), "changeSet", null);
} catch (IOException x) {
LOG.log(Level.WARNING, "could not parse changelog for {0}: {1}", new Object[] {build, x});
return Collections.emptyList();
}
class GitItem implements HudsonJobChangeItem {
final Element itemXML;
GitItem(Element itemXML) {
this.itemXML = itemXML;
}
@Override public String getUser() {
return Utilities.xpath("author/fullName", itemXML);
}
@Override public String getMessage() {
return Utilities.xpath("msg", itemXML);
}
@Override public Collection<? extends HudsonJobChangeFile> getFiles() {
class GitFile implements HudsonJobChangeFile {
final Element fileXML;
GitFile(Element fileXML) {
this.fileXML = fileXML;
}
@Override public String getName() {
return Utilities.xpath("file", fileXML);
}
@Override public EditType getEditType() {
return EditType.valueOf(Utilities.xpath("editType", fileXML));
}
@Override public OutputListener hyperlink() {
return null; // XXX no idea how to look up remote content from a Git URL generally
}
}
List<GitFile> files = new ArrayList<GitFile>();
NodeList nl = itemXML.getElementsByTagName("path");
for (int i = 0; i < nl.getLength(); i++) {
files.add(new GitFile((Element) nl.item(i)));
}
return files;
}
}
String kind = Utilities.xpath("kind", changeSet); // #224993 //NOI18N
if (kind != null && !"git".equals(kind)) { //NOI18N
return null; //not a git changelog
}
List<GitItem> items = new ArrayList<GitItem>();
NodeList nl = changeSet.getElementsByTagName("item");
for (int i = 0; i < nl.getLength(); i++) {
Element itemXML = (Element) nl.item(i);
if (kind == null && !looksLikeGitChangeLog(itemXML)) {
return null; // does not look like a Git changelog
}
items.add(new GitItem(itemXML));
}
if (items.isEmpty()) {
return null; // might not be a Git changelog
}
return items;
}
/**
* @return false if the item does not look like a Git changelog, true
* otherwise.
*/
private boolean looksLikeGitChangeLog(Element itemXML) {
Element idE = XMLUtil.findElement(itemXML, "id", null); //NOI18N
return idE != null
&& XMLUtil.findText(idE).matches("[0-9a-f]{40}"); //NOI18N
}
static @CheckForNull URI getRemoteOrigin(URI repository, @NullAllowed HudsonJob job) {
assert repository.toString().endsWith("/");
URI cfg = repository.resolve(".git/config");
String origin = null;
ClassLoader l = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(HudsonGitSCM.class.getClassLoader()); // #141364
try {
ConnectionBuilder cb = new ConnectionBuilder();
if (job != null) {
cb = cb.job(job);
}
InputStream is = cb.url(cfg.toURL()).connection().getInputStream();
try {
Ini ini = new Ini(is);
Ini.Section section = ini.get("remote \"origin\"");
if (section != null) {
origin = section.get("url");
}
} finally {
is.close();
}
} catch (InvalidFileFormatException x) {
LOG.log(Level.FINE, "{0} was malformed, perhaps no workspace: {1}", new Object[] {cfg, x});
return null;
} catch (FileNotFoundException x) {
LOG.log(Level.FINE, "{0} not found", cfg);
return null;
} catch (Exception x) {
LOG.log(Level.WARNING, "Could not parse " + cfg, x);
return null;
} finally {
Thread.currentThread().setContextClassLoader(l);
}
if (origin == null) {
LOG.log(Level.FINE, "{0} does not specify remote under name ''origin''", cfg);
return null;
}
Matcher m = Pattern.compile("([^@:/]+@[^/:]+):(.+)").matcher(origin);
if (m.matches()) {
origin = "ssh://" + m.group(1) + "/" + m.group(2);
}
// XXX deal with local file paths; need to translate to file: URLs
// XXX any further processing needed? absolutization, password stripping?
try {
return new URI(origin);
} catch (URISyntaxException x) {
LOG.log(Level.FINE, "could not load origin from {0}: {1}", new Object[] {cfg, x});
return null;
}
}
// http://stackoverflow.com/questions/3189520/hudson-git-plugin-wont-clone-repo-on-linux
static @CheckForNull String roReplacement(String url) {
String prefix = "ssh://git@github.com/";
if (url.startsWith(prefix)) {
return "git://github.com/" + url.substring(prefix.length());
}
Matcher m = Pattern.compile("ssh://.+@git[.]((?:kenai[.]com|java[.]net).+)").matcher(url);
if (m.matches()) {
return "git://" + m.group(1);
}
return null;
}
}