提交 6815152c 编写于 作者: J Jesse Glick

Merge branch 'master' into TransientActionFactory-JENKINS-18224

......@@ -55,6 +55,11 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class='major rfe'>
Added infrastructure for moving items into or out of folders.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-20008">issue 20008</a>)
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-18028">issue 18028</a>)
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-18680">issue 18680</a>)
<li class=bug>
<i>Apply</i> buttons did not work in Internet Explorer in compatibility mode.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-19826">issue 19826</a>)
......
......@@ -225,6 +225,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
+ " already exists");
String oldName = this.name;
String oldFullName = getFullName();
File oldRoot = this.getRootDir();
doSetName(newName);
......@@ -295,8 +296,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
callOnRenamed(newName, parent, oldName);
for (ItemListener l : ItemListener.all())
l.onRenamed(this, oldName, newName);
ItemListener.fireLocationChange(this, oldFullName);
}
}
}
......
......@@ -81,11 +81,6 @@ public class AllView extends View {
return ""; // there's no configuration page
}
@Override
public void onJobRenamed(Item item, String oldName, String newName) {
// noop
}
@Override
protected void submit(StaplerRequest req) throws IOException, ServletException, FormException {
// noop
......
......@@ -727,7 +727,7 @@ public class Fingerprint implements ModelObject, Saveable {
@Extension
public static final class ProjectRenameListener extends ItemListener {
@Override
public void onRenamed(Item item, String oldName, String newName) {
public void onLocationChanged(Item item, String oldName, String newName) {
if (item instanceof AbstractProject) {
AbstractProject p = Jenkins.getInstance().getItemByFullName(newName, AbstractProject.class);
if (p != null) {
......
......@@ -23,6 +23,7 @@
*/
package hudson.model;
import hudson.model.listeners.ItemListener;
import java.io.IOException;
import java.util.Collection;
import java.io.File;
......@@ -76,6 +77,7 @@ public interface ItemGroup<T extends Item> extends PersistenceRoot, ModelObject
/**
* Internal method. Called by {@link Item}s when they are renamed by users.
* This is <em>not</em> expected to call {@link ItemListener#onRenamed}, inconsistent with {@link #onDeleted}.
*/
void onRenamed(T item, String oldName, String newName) throws IOException;
......
......@@ -99,7 +99,13 @@ public abstract class ItemGroupMixIn {
CopyOnWriteMap.Tree<K,V> configurations = new CopyOnWriteMap.Tree<K,V>();
for (File subdir : subdirs) {
try {
V item = (V) Items.load(parent,subdir);
// Try to retain the identity of an existing child object if we can.
V item = (V) parent.getItem(subdir.getName());
if (item == null) {
item = (V) Items.load(parent,subdir);
} else {
item.onLoad(parent, subdir.getName());
}
configurations.put(key.call(item), item);
} catch (IOException e) {
Logger.getLogger(ItemGroupMixIn.class.getName()).log(Level.WARNING, "could not load " + subdir, e);
......
......@@ -30,6 +30,7 @@ import hudson.matrix.MatrixProject;
import hudson.matrix.MatrixConfiguration;
import hudson.XmlFile;
import hudson.matrix.Axis;
import hudson.model.listeners.ItemListener;
import hudson.remoting.Callable;
import hudson.triggers.Trigger;
import hudson.util.DescriptorList;
......@@ -42,6 +43,8 @@ import java.io.File;
import java.io.IOException;
import java.util.*;
import javax.annotation.CheckForNull;
import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import org.apache.commons.io.FileUtils;
/**
* Convenience methods related to {@link Item}.
......@@ -179,8 +182,8 @@ public class Items {
}
/**
* Compute the relative name of list of items after a rename occurred. Used to manage job references as names in
* plugins to support {@link hudson.model.listeners.ItemListener#onRenamed(hudson.model.Item, String, String)}.
* Computes the relative name of list of items after a rename or move occurred.
* Used to manage job references as names in plugins to support {@link hudson.model.listeners.ItemListener#onLocationChanged}.
* <p>
* In a hierarchical context, when a plugin has a reference to a job as <code>../foo/bar</code> this method will
* handle the relative path as "foo" is renamed to "zot" to compute <code>../zot/bar</code>
......@@ -200,8 +203,11 @@ public class Items {
String canonicalName = getCanonicalName(context, relativeName);
if (canonicalName.equals(oldFullName) || canonicalName.startsWith(oldFullName+'/')) {
String newCanonicalName = newFullName + canonicalName.substring(oldFullName.length());
// relative name points to the renamed item, let's compute the new relative name
newValue.add( computeRelativeNameAfterRenaming(canonicalName, newCanonicalName, relativeName) );
if (relativeName.startsWith("/")) {
newValue.add("/" + newCanonicalName);
} else {
newValue.add(getRelativeNameFrom(newCanonicalName, context.getFullName()));
}
} else {
newValue.add(relativeName);
}
......@@ -209,39 +215,56 @@ public class Items {
return StringUtils.join(newValue, ",");
}
/**
* Compute the relative name of an Item after renaming
*/
private static String computeRelativeNameAfterRenaming(String oldFullName, String newFullName, String relativeName) {
String[] a = oldFullName.split("/");
String[] n = newFullName.split("/");
assert a.length == n.length;
String[] r = relativeName.split("/");
int j = a.length-1;
for(int i=r.length-1;i>=0;i--) {
String part = r[i];
if (part.equals("") && i==0) {
continue;
}
if (part.equals(".")) {
continue;
}
if (part.equals("..")) {
j--;
continue;
}
if (part.equals(a[j])) {
r[i] = n[j];
j--;
// Had difficulty adapting the version in Functions to use no live items, so rewrote it:
static String getRelativeNameFrom(String itemFullName, String groupFullName) {
String[] itemFullNameA = itemFullName.isEmpty() ? new String[0] : itemFullName.split("/");
String[] groupFullNameA = groupFullName.isEmpty() ? new String[0] : groupFullName.split("/");
for (int i = 0; ; i++) {
if (i == itemFullNameA.length) {
if (i == groupFullNameA.length) {
// itemFullName and groupFullName are identical
return ".";
} else {
// itemFullName is an ancestor of groupFullName; insert ../ for rest of groupFullName
StringBuilder b = new StringBuilder();
for (int j = 0; j < groupFullNameA.length - itemFullNameA.length; j++) {
if (j > 0) {
b.append('/');
}
b.append("..");
}
return b.toString();
}
} else if (i == groupFullNameA.length) {
// groupFullName is an ancestor of itemFullName; insert rest of itemFullName
StringBuilder b = new StringBuilder();
for (int j = i; j < itemFullNameA.length; j++) {
if (j > i) {
b.append('/');
}
b.append(itemFullNameA[j]);
}
return b.toString();
} else if (itemFullNameA[i].equals(groupFullNameA[i])) {
// identical up to this point
continue;
} else {
// first mismatch; insert ../ for rest of groupFullName, then rest of itemFullName
StringBuilder b = new StringBuilder();
for (int j = i; j < groupFullNameA.length; j++) {
if (j > i) {
b.append('/');
}
b.append("..");
}
for (int j = i; j < itemFullNameA.length; j++) {
b.append('/').append(itemFullNameA[j]);
}
return b.toString();
}
}
return StringUtils.join(r, '/');
}
/**
* Loads a {@link Item} from a config file.
*
......@@ -330,6 +353,42 @@ public class Items {
return Jenkins.getInstance().getItem(nearest, context, type);
}
/**
* Moves an item between folders (or top level).
* Fires all relevant events but does not verify that the item’s directory is not currently being used in some way (for example by a running build).
* Does not check any permissions.
* @param item some item (job or folder)
* @param destination the destination of the move (a folder or {@link Jenkins}); not the current parent (or you could just call {@link AbstractItem#renameTo})
* @return the new item (usually the same object as {@code item})
* @throws IOException if the move fails, or some subsequent step fails (directory might have already been moved)
* @throws IllegalArgumentException if the move would really be a rename, or the destination cannot accept the item, or the destination already has an item of that name
* @since 1.548
*/
public static <I extends AbstractItem & TopLevelItem> I move(I item, DirectlyModifiableTopLevelItemGroup destination) throws IOException, IllegalArgumentException {
DirectlyModifiableTopLevelItemGroup oldParent = (DirectlyModifiableTopLevelItemGroup) item.getParent();
if (oldParent == destination) {
throw new IllegalArgumentException();
}
// TODO verify that destination is to not equal to, or inside, item
if (!destination.canAdd(item)) {
throw new IllegalArgumentException();
}
String name = item.getName();
if (destination.getItem(name) != null) {
throw new IllegalArgumentException(name + " already exists");
}
String oldFullName = item.getFullName();
// TODO AbstractItem.renameTo has a more baroque implementation; factor it out into a utility method perhaps?
File destDir = destination.getRootDirFor(item);
FileUtils.forceMkdir(destDir.getParentFile());
FileUtils.moveDirectory(item.getRootDir(), destDir);
oldParent.remove(item);
I newItem = destination.add(item, name);
newItem.onLoad(destination, name);
ItemListener.fireLocationChange(newItem, oldFullName);
return newItem;
}
/**
* Used to load/save job configuration.
*
......
......@@ -28,6 +28,7 @@ import hudson.Extension;
import hudson.Util;
import hudson.diagnosis.OldDataMonitor;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.ItemListener;
import hudson.util.CaseInsensitiveComparator;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
......@@ -37,13 +38,18 @@ import hudson.views.ViewJobFilter;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
......@@ -302,12 +308,6 @@ public class ListView extends View implements Saveable {
return HttpResponses.ok();
}
@Override
public synchronized void onJobRenamed(Item item, String oldName, String newName) {
if(jobNames.remove(oldName) && newName!=null)
jobNames.add(newName);
}
/**
* Handles the configuration submission.
*
......@@ -387,4 +387,56 @@ public class ListView extends View implements Saveable {
public static List<ListViewColumn> getDefaultColumns() {
return ListViewColumn.createDefaultInitialColumnList();
}
@Restricted(NoExternalUse.class)
@Extension public static final class Listener extends ItemListener {
@Override public void onLocationChanged(Item item, String oldFullName, String newFullName) {
for (Item g : Jenkins.getInstance().getAllItems()) {
if (g instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) g;
for (View v : vg.getViews()) {
if (v instanceof ListView) {
ListView lv = (ListView) v;
synchronized (lv) {
Set<String> oldJobNames = new HashSet<String>(lv.jobNames);
lv.jobNames.clear();
for (String oldName : oldJobNames) {
lv.jobNames.add(Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, oldName, vg.getItemGroup()));
}
if (!oldJobNames.equals(lv.jobNames)) {
try {
g.save();
} catch (IOException x) {
Logger.getLogger(ListView.class.getName()).log(Level.WARNING, null, x);
}
}
}
}
}
}
}
}
@Override public void onDeleted(Item item) {
for (Item g : Jenkins.getInstance().getAllItems()) {
if (g instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) g;
for (View v : vg.getViews()) {
if (v instanceof ListView) {
ListView lv = (ListView) v;
synchronized (lv) {
if (lv.jobNames.remove(item.getRelativeNameFrom(vg.getItemGroup()))) {
try {
g.save();
} catch (IOException x) {
Logger.getLogger(ListView.class.getName()).log(Level.WARNING, null, x);
}
}
}
}
}
}
}
}
}
}
......@@ -86,11 +86,6 @@ public class MyView extends View {
return ""; // there's no configuration page
}
@Override
public void onJobRenamed(Item item, String oldName, String newName) {
// noop
}
@Override
protected void submit(StaplerRequest req) throws IOException, ServletException, FormException {
// noop
......
......@@ -89,13 +89,6 @@ public class ProxyView extends View implements StaplerFallback {
return getProxiedView().contains(item);
}
@Override
public void onJobRenamed(Item item, String oldName, String newName) {
if (oldName.equals(proxiedViewName)) {
proxiedViewName = newName;
}
}
@Override
protected void submit(StaplerRequest req) throws IOException, ServletException, FormException {
String proxiedViewName = req.getSubmittedForm().getString("proxiedViewName");
......
......@@ -119,14 +119,7 @@ public class TreeView extends View implements ViewGroup {
return null;
}
@Override
public synchronized void onJobRenamed(Item item, String oldName, String newName) {
if(jobNames.remove(oldName) && newName!=null)
jobNames.add(newName);
// forward to children
for (View v : views)
v.onJobRenamed(item,oldName,newName);
}
// TODO listen for changes that might affect jobNames
protected void submit(StaplerRequest req) throws IOException, ServletException, FormException {
}
......
......@@ -581,21 +581,9 @@ public abstract class View extends AbstractModelObject implements AccessControll
return getACL().hasPermission(p);
}
/**
* Called when a job name is changed or deleted.
*
* <p>
* If this view contains this job, it should update the view membership so that
* the renamed job will remain in the view, and the deleted job is removed.
*
* @param item
* The item whose name is being changed.
* @param oldName
* Old name of the item. Always non-null.
* @param newName
* New name of the item, if the item is renamed. Or null, if the item is removed.
*/
public abstract void onJobRenamed(Item item, String oldName, String newName);
/** @deprecated Does not work properly with moved jobs. Use {@link ItemListener#onLocationChanged} instead. */
@Deprecated
public void onJobRenamed(Item item, String oldName, String newName) {}
@ExportedBean(defaultVisibility=2)
public static final class UserInfo implements Comparable<UserInfo> {
......
......@@ -28,6 +28,8 @@ import hudson.ExtensionList;
import hudson.Extension;
import jenkins.model.Jenkins;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
/**
* Receives notifications about CRUD operations of {@link Item}.
......@@ -82,7 +84,7 @@ public class ItemListener implements ExtensionPoint {
/**
* Called after a job is renamed.
*
* Most implementers should rather use {@link #onLocationChanged}.
* @param item
* The job being renamed.
* @param oldName
......@@ -94,6 +96,28 @@ public class ItemListener implements ExtensionPoint {
public void onRenamed(Item item, String oldName, String newName) {
}
/**
* Called after an item’s fully-qualified location has changed.
* This might be because:
* <ul>
* <li>This item was renamed.
* <li>Some ancestor folder was renamed.
* <li>This item was moved between folders (or from a folder to Jenkins root or vice-versa).
* <li>Some ancestor folder was moved.
* </ul>
* Where applicable, {@link #onRenamed} will already have been called on this item or an ancestor.
* And where applicable, {@link #onLocationChanged} will already have been called on its ancestors.
* <p>This method should be used (instead of {@link #onRenamed}) by any code
* which seeks to keep (absolute) references to items up to date:
* if a persisted reference matches {@code oldFullName}, replace it with {@code newFullName}.
* @param item an item whose absolute position is now different
* @param oldFullName the former {@link Item#getFullName}
* @param newFullName the current {@link Item#getFullName}
* @see Items#computeRelativeNamesAfterRenaming
* @since 1.548
*/
public void onLocationChanged(Item item, String oldFullName, String newFullName) {}
/**
* Called after a job has its configuration updated.
*
......@@ -141,4 +165,50 @@ public class ItemListener implements ExtensionPoint {
for (ItemListener l : all())
l.onUpdated(item);
}
/** @since 1.548 */
public static void fireOnDeleted(Item item) {
for (ItemListener l : all()) {
l.onDeleted(item);
}
}
/**
* Calls {@link #onRenamed} and {@link #onLocationChanged} as appropriate.
* @param rootItem the topmost item whose location has just changed
* @param oldFullName the previous {@link Item#getFullName}
* @since 1.548
*/
public static void fireLocationChange(Item rootItem, String oldFullName) {
String prefix = rootItem.getParent().getFullName();
if (!prefix.isEmpty()) {
prefix += '/';
}
String newFullName = rootItem.getFullName();
assert newFullName.startsWith(prefix);
int prefixS = prefix.length();
if (oldFullName.startsWith(prefix) && oldFullName.indexOf('/', prefixS) == -1) {
String oldName = oldFullName.substring(prefixS);
String newName = rootItem.getName();
assert newName.equals(newFullName.substring(prefixS));
for (ItemListener l : all()) {
l.onRenamed(rootItem, oldName, newName);
}
}
for (ItemListener l : all()) {
l.onLocationChanged(rootItem, oldFullName, newFullName);
}
if (rootItem instanceof ItemGroup) {
for (Item child : Items.getAllItems((ItemGroup) rootItem, Item.class)) {
String childNew = child.getFullName();
assert childNew.startsWith(newFullName);
assert childNew.charAt(newFullName.length()) == '/';
String childOld = oldFullName + childNew.substring(newFullName.length());
for (ItemListener l : all()) {
l.onLocationChanged(child, childOld, childNew);
}
}
}
}
}
......@@ -46,7 +46,6 @@ import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.listeners.ItemListener;
import hudson.tasks.BuildTrigger.DescriptorImpl.ItemListenerImpl;
import hudson.util.FormValidation;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
......@@ -243,11 +242,8 @@ public class BuildTrigger extends Recorder implements DependencyDeclarer {
return true;
}
/**
* Called from {@link ItemListenerImpl} when a job is renamed.
*
* @return true if this {@link BuildTrigger} is changed and needs to be saved.
*/
/** @deprecated Does not handle folder moves. */
@Deprecated
public boolean onJobRenamed(String oldName, String newName) {
// quick test
if(!childProjects.contains(oldName))
......@@ -355,17 +351,19 @@ public class BuildTrigger extends Recorder implements DependencyDeclarer {
@Extension
public static class ItemListenerImpl extends ItemListener {
@Override
public void onRenamed(Item item, String oldName, String newName) {
public void onLocationChanged(Item item, String oldFullName, String newFullName) {
// update BuildTrigger of other projects that point to this object.
// can't we generalize this?
for( Project<?,?> p : Jenkins.getInstance().getAllItems(Project.class) ) {
BuildTrigger t = p.getPublishersList().get(BuildTrigger.class);
if(t!=null) {
if(t.onJobRenamed(oldName,newName)) {
String cp2 = Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, t.childProjects, p.getParent());
if (!cp2.equals(t.childProjects)) {
t.childProjects = cp2;
try {
p.save();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to persist project setting during rename from "+oldName+" to "+newName,e);
LOGGER.log(Level.WARNING, "Failed to persist project setting during rename from "+oldFullName+" to "+newFullName,e);
}
}
}
......
/*
* The MIT License
*
* Copyright 2013 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model;
import hudson.model.Item;
import hudson.model.TopLevelItem;
import hudson.model.listeners.ItemListener;
import java.io.IOException;
/**
* Item group which supports items being directly moved in or out of it.
* @since 1.548
*/
public interface DirectlyModifiableTopLevelItemGroup extends ModifiableTopLevelItemGroup {
/**
* Returns true if a particular item may be added to this group.
* @param item an item currently elsewhere
* @return true if {@link #add} may be called with this item
*/
boolean canAdd(TopLevelItem item);
/**
* Adds an item to this group.
* Unlike {@link Jenkins#putItem} this does not try to call {@link Item#delete} on an existing item, nor does it fire {@link ItemListener#onCreated}, nor check permissions.
* Normally you would call {@link Item#onLoad} after calling this method (the implementation is not expected to do so).
* @param <I> the kind of item
* @param item an item to add which is currently elsewhere
* @param name the desired item name in this group (might simply be the original {@link Item#getName})
* @return normally the same {@code item}, but might be a new cppy if necessary
* @throws IOException if adding fails
* @throws IllegalArgumentException if {@link #canAdd} is false, or an item with this name already exists, or this item is as yet unnamed
*/
<I extends TopLevelItem> I add(I item, String name) throws IOException, IllegalArgumentException;
/**
* Removes an item from this group.
* Unlike {@link #onDeleted} this is not expected to fire any events.
* @param item an item which was part of this group
* @throws IOException if removing fails
* @throws IllegalArgumentException if this was not part of the group to begin with
*/
void remove(TopLevelItem item) throws IOException, IllegalArgumentException;
}
......@@ -305,7 +305,7 @@ import javax.annotation.Nullable;
* @author Kohsuke Kawaguchi
*/
@ExportedBean
public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGroup, StaplerProxy, StaplerFallback,
public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLevelItemGroup, StaplerProxy, StaplerFallback,
ModifiableViewGroup, AccessControlled, DescriptorByNameOwner,
ModelObjectWithContextMenu, ModelObjectWithChildren {
private transient final Queue queue;
......@@ -474,7 +474,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
* Active {@link Cloud}s.
*/
public final Hudson.CloudList clouds = new Hudson.CloudList(this);
public static class CloudList extends DescribableList<Cloud,Descriptor<Cloud>> {
public CloudList(Jenkins h) {
super(h);
......@@ -2436,6 +2436,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
items.remove(oldName);
items.put(newName,job);
// For compatibility with old views:
for (View v : views)
v.onJobRenamed(job, oldName, newName);
save();
......@@ -2445,15 +2446,31 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
* Called in response to {@link Job#doDoDelete(StaplerRequest, StaplerResponse)}
*/
public void onDeleted(TopLevelItem item) throws IOException {
for (ItemListener l : ItemListener.all())
l.onDeleted(item);
ItemListener.fireOnDeleted(item);
items.remove(item.getName());
// For compatibility with old views:
for (View v : views)
v.onJobRenamed(item, item.getName(), null);
save();
}
@Override public boolean canAdd(TopLevelItem item) {
return true;
}
@Override synchronized public <I extends TopLevelItem> I add(I item, String name) throws IOException, IllegalArgumentException {
if (items.containsKey(name)) {
throw new IllegalArgumentException("already an item '" + name + "'");
}
items.put(name, item);
return item;
}
@Override public void remove(TopLevelItem item) throws IOException, IllegalArgumentException {
items.remove(item.getName());
}
public FingerprintMap getFingerprintMap() {
return fingerprintMap;
}
......
......@@ -60,6 +60,30 @@ public class ItemsTest {
assertEquals("foo-renamed,foo_bar", Items.computeRelativeNamesAfterRenaming("foo", "foo-renamed", "foo,foo_bar", root ));
// Handle moves too:
assertEquals("../nue/dir/j", Items.computeRelativeNamesAfterRenaming("dir", "nue/dir", "../dir/j", foo));
assertEquals("../dir/j", Items.computeRelativeNamesAfterRenaming("nue/dir", "dir", "../nue/dir/j", foo));
assertEquals("../top2/dir/j", Items.computeRelativeNamesAfterRenaming("top1/dir", "top2/dir", "../top1/dir/j", foo));
assertEquals("nue/dir/j", Items.computeRelativeNamesAfterRenaming("dir", "nue/dir", "dir/j", root));
assertEquals("dir/j", Items.computeRelativeNamesAfterRenaming("nue/dir", "dir", "nue/dir/j", root));
assertEquals("top2/dir/j", Items.computeRelativeNamesAfterRenaming("top1/dir", "top2/dir", "top1/dir/j", root));
assertEquals("/nue/dir/j", Items.computeRelativeNamesAfterRenaming("dir", "nue/dir", "/dir/j", foo));
assertEquals("/dir/j", Items.computeRelativeNamesAfterRenaming("nue/dir", "dir", "/nue/dir/j", foo));
assertEquals("/top2/dir/j", Items.computeRelativeNamesAfterRenaming("top1/dir", "top2/dir", "/top1/dir/j", foo));
assertEquals("sister", Items.computeRelativeNamesAfterRenaming("fooq", "foo", "sister", foo));
assertEquals("/foo/sister", Items.computeRelativeNamesAfterRenaming("fooq", "foo", "/fooq/sister", foo));
}
@Test public void getRelativeNameFrom() {
assertEquals("foo", Items.getRelativeNameFrom("foo", ""));
assertEquals("foo/bar", Items.getRelativeNameFrom("foo/bar", ""));
assertEquals("../bar", Items.getRelativeNameFrom("bar", "foo"));
assertEquals("../baz", Items.getRelativeNameFrom("foo/baz", "foo/bar"));
assertEquals("bar", Items.getRelativeNameFrom("foo/bar", "foo"));
assertEquals(".", Items.getRelativeNameFrom("foo/bar", "foo/bar"));
assertEquals("../..", Items.getRelativeNameFrom("foo", "foo/bar/baz"));
assertEquals("bar/baz", Items.getRelativeNameFrom("foo/bar/baz", "foo"));
assertEquals("../quux/hey", Items.getRelativeNameFrom("foo/bar/quux/hey", "foo/bar/baz"));
}
}
......@@ -177,10 +177,6 @@ public class ViewTest {
return false;
}
@Override
public void onJobRenamed(Item item, String oldName, String newName) {
}
@Override
protected void submit(StaplerRequest req) throws IOException, ServletException, FormException {
}
......
......@@ -39,6 +39,7 @@ import hudson.model.TopLevelItem;
import hudson.model.TopLevelItemDescriptor;
import hudson.model.View;
import hudson.model.ViewGroupMixIn;
import hudson.model.listeners.ItemListener;
import hudson.util.Function1;
import hudson.views.DefaultViewsTabBar;
import hudson.views.ViewsTabBar;
......@@ -54,8 +55,8 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.servlet.ServletException;
import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import jenkins.model.Jenkins;
import jenkins.model.ModifiableTopLevelItemGroup;
import org.kohsuke.stapler.StaplerFallback;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -69,7 +70,7 @@ import org.kohsuke.stapler.WebMethod;
* @since 1.494
*/
@SuppressWarnings({"unchecked", "rawtypes"}) // the usual API mistakes
public class MockFolder extends AbstractItem implements ModifiableTopLevelItemGroup, TopLevelItem, ModifiableViewGroup, StaplerFallback {
public class MockFolder extends AbstractItem implements DirectlyModifiableTopLevelItemGroup, TopLevelItem, ModifiableViewGroup, StaplerFallback {
private transient Map<String,TopLevelItem> items = new TreeMap<String,TopLevelItem>();
private final List<View> views = new ArrayList<View>(Collections.singleton(new AllView("All", this)));
......@@ -182,8 +183,28 @@ public class MockFolder extends AbstractItem implements ModifiableTopLevelItemGr
items.put(newName, item);
}
@Override public void renameTo(String newName) throws IOException {
super.renameTo(newName); // just to make it public
}
@Override public void onDeleted(TopLevelItem item) throws IOException {
// could call ItemListener.onDeleted
ItemListener.fireOnDeleted(item);
items.remove(item.getName());
}
@Override public boolean canAdd(TopLevelItem item) {
return true;
}
@Override synchronized public <I extends TopLevelItem> I add(I item, String name) throws IOException, IllegalArgumentException {
if (items.containsKey(name)) {
throw new IllegalArgumentException("already an item '" + name + "'");
}
items.put(name, item);
return item;
}
@Override public void remove(TopLevelItem item) throws IOException, IllegalArgumentException {
items.remove(item.getName());
}
......
......@@ -383,6 +383,11 @@ public class AbstractProjectTest extends HudsonTestCase {
p.renameTo("edited");
p._getRuns().purgeCache();
assertEquals(1, p.getBuilds().size());
def d = jenkins.createProject(MockFolder.class, "d");
Items.move(p, d);
assertEquals(p, jenkins.getItemByFullName("d/edited"));
p._getRuns().purgeCache();
assertEquals(1, p.getBuilds().size());
}
@Bug(17575)
......
......@@ -24,11 +24,17 @@
package hudson.model;
import static org.junit.Assert.*;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.Functions;
import hudson.matrix.AxisList;
import hudson.matrix.MatrixProject;
import hudson.matrix.TextAxis;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Bug;
......@@ -38,13 +44,6 @@ import org.jvnet.hudson.test.MockFolder;
import org.jvnet.hudson.test.recipes.LocalData;
import org.xml.sax.SAXException;
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import hudson.matrix.AxisList;
import hudson.matrix.MatrixProject;
import hudson.matrix.TextAxis;
import java.util.Collections;
public class ListViewTest {
@Rule public JenkinsRule j = new JenkinsRule();
......@@ -112,4 +111,28 @@ public class ListViewTest {
assertEquals(Collections.singletonList(mp), v.getItems());
}
@Bug(18680)
@Test public void renamesMovesAndDeletes() throws Exception {
MockFolder top = j.createFolder("top");
MockFolder sub = top.createProject(MockFolder.class, "sub");
FreeStyleProject p1 = top.createProject(FreeStyleProject.class, "p1");
FreeStyleProject p2 = sub.createProject(FreeStyleProject.class, "p2");
FreeStyleProject p3 = top.createProject(FreeStyleProject.class, "p3");
ListView v = new ListView("v");
top.addView(v);
v.add(p1);
v.add(p2);
v.add(p3);
assertEquals(new HashSet<TopLevelItem>(Arrays.asList(p1, p2, p3)), new HashSet<TopLevelItem>(v.getItems()));
sub.renameTo("lower");
MockFolder stuff = top.createProject(MockFolder.class, "stuff");
Items.move(p1, stuff);
p3.delete();
Thread.sleep(500); // TODO working around crappy JENKINS-19446 fix
top.createProject(FreeStyleProject.class, "p3");
assertEquals(new HashSet<TopLevelItem>(Arrays.asList(p1, p2)), new HashSet<TopLevelItem>(v.getItems()));
top.renameTo("upper");
assertEquals(new HashSet<TopLevelItem>(Arrays.asList(p1, p2)), new HashSet<TopLevelItem>(v.getItems()));
}
}
......@@ -25,9 +25,12 @@
package org.jvnet.hudson.test;
import hudson.model.FreeStyleProject;
import org.junit.Test;
import hudson.model.Item;
import hudson.model.Items;
import hudson.model.listeners.ItemListener;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
public class MockFolderTest {
......@@ -39,4 +42,68 @@ public class MockFolderTest {
assertEquals("dir/p", p.getFullName());
}
@Test public void moving() throws Exception {
MockFolder top = j.createFolder("top");
FreeStyleProject p = top.createProject(FreeStyleProject.class, "p");
MockFolder sub = top.createProject(MockFolder.class, "sub");
assertNews("created=top created=top/p created=top/sub");
Items.move(p, j.jenkins);
assertEquals(j.jenkins, p.getParent());
assertEquals(p, j.jenkins.getItem("p"));
assertNull(top.getItem("p"));
assertNews("moved=p;from=top/p");
Items.move(p, sub);
assertEquals(sub, p.getParent());
assertEquals(p, sub.getItem("p"));
assertNull(j.jenkins.getItem("p"));
assertNews("moved=top/sub/p;from=p");
Items.move(sub, j.jenkins);
assertEquals(sub, p.getParent());
assertEquals(p, sub.getItem("p"));
assertEquals(j.jenkins, sub.getParent());
assertEquals(sub, j.jenkins.getItem("sub"));
assertNull(top.getItem("sub"));
assertNews("moved=sub;from=top/sub moved=sub/p;from=top/sub/p");
Items.move(sub, top);
assertNews("moved=top/sub;from=sub moved=top/sub/p;from=sub/p");
assertEquals(sub, top.getItem("sub"));
sub.renameTo("lower");
assertNews("renamed=top/lower;from=sub moved=top/lower;from=top/sub moved=top/lower/p;from=top/sub/p");
top.renameTo("upper");
assertNews("renamed=upper;from=top moved=upper;from=top moved=upper/lower;from=top/lower moved=upper/lower/p;from=top/lower/p");
assertEquals(p, sub.getItem("p"));
p.renameTo("j");
assertNews("renamed=upper/lower/j;from=p moved=upper/lower/j;from=upper/lower/p");
top.renameTo("upperz");
assertNews("renamed=upperz;from=upper moved=upperz;from=upper moved=upperz/lower;from=upper/lower moved=upperz/lower/j;from=upper/lower/j");
assertEquals(sub, top.getItem("lower"));
sub.renameTo("upperzee");
assertNews("renamed=upperz/upperzee;from=lower moved=upperz/upperzee;from=upperz/lower moved=upperz/upperzee/j;from=upperz/lower/j");
Items.move(sub, j.jenkins);
assertNews("moved=upperzee;from=upperz/upperzee moved=upperzee/j;from=upperz/upperzee/j");
assertEquals(p, j.jenkins.getItemByFullName("upperzee/j"));
}
private void assertNews(String expected) {
L l = j.jenkins.getExtensionList(ItemListener.class).get(L.class);
assertEquals(expected, l.b.toString().trim());
l.b.delete(0, l.b.length());
}
@TestExtension("moving") public static class L extends ItemListener {
final StringBuilder b = new StringBuilder();
@Override public void onCreated(Item item) {
b.append(" created=").append(item.getFullName());
}
@Override public void onDeleted(Item item) {
b.append(" deleted=").append(item.getFullName());
}
@Override public void onRenamed(Item item, String oldName, String newName) {
assertEquals(item.getName(), newName);
b.append(" renamed=").append(item.getFullName()).append(";from=").append(oldName);
}
@Override public void onLocationChanged(Item item, String oldFullName, String newFullName) {
assertEquals(item.getFullName(), newFullName);
b.append(" moved=").append(newFullName).append(";from=").append(oldFullName);
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册