diff --git a/changelog.html b/changelog.html
index 6c6870070e43c4149f011e4bdada624f29d9d5d5..43ffa1730945a40828a1d52d025a7380024de137 100644
--- a/changelog.html
+++ b/changelog.html
@@ -55,6 +55,8 @@ Upcoming changes
+ -
+ Views can now include jobs located within folders
-
diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java
index a330a042bfba8edad253fa7ec7269e3ba901e92a..fc263f5efad7df4d1c23769c08599eed4a9f23fc 100644
--- a/core/src/main/java/hudson/Functions.java
+++ b/core/src/main/java/hudson/Functions.java
@@ -903,7 +903,7 @@ public class Functions {
ItemGroup ig = i.getParent();
url = i.getShortUrl()+url;
- if(ig== Jenkins.getInstance()) {
+ if(ig== Jenkins.getInstance() || (view != null && ig == view.getOwner())) {
assert i instanceof TopLevelItem;
if(view!=null && view.contains((TopLevelItem)i)) {
// if p and the current page belongs to the same view, then return a relative path
diff --git a/core/src/main/java/hudson/model/AbstractItem.java b/core/src/main/java/hudson/model/AbstractItem.java
index 07cb6825df0fae267f457732796b1fa64e34759d..ca00e912c4f0e967c20c0bb74d18ecbbf4147b36 100644
--- a/core/src/main/java/hudson/model/AbstractItem.java
+++ b/core/src/main/java/hudson/model/AbstractItem.java
@@ -43,6 +43,8 @@ import hudson.util.AtomicFileWriter;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import jenkins.model.Jenkins;
+
+import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.WebMethod;
@@ -54,6 +56,7 @@ import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import java.util.StringTokenizer;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
@@ -326,6 +329,21 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
if(n.length()==0) return getDisplayName();
else return n+" \u00BB "+getDisplayName();
}
+
+ public String getRelativeDisplayNameFrom(ItemGroup p) {
+ String relativeName = getRelativeNameFrom(p);
+ if (relativeName == null) return null;
+ return relativeName.replace("/", " \u00BB ");
+ }
+
+ /**
+ * This method only exists to disambiguate getRelativeNameFrom(Itemgroup) and getRelativeNameFrom(Item)
+ * @param p
+ * @return
+ */
+ public String getRelativeNameFromGroup(ItemGroup p) {
+ return getRelativeNameFrom(p);
+ }
public String getRelativeNameFrom(ItemGroup p) {
// first list up all the parents
@@ -367,7 +385,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
/**
* Called right after when a {@link Item} is loaded from disk.
- * This is an opporunity to do a post load processing.
+ * This is an opportunity to do a post load processing.
*/
public void onLoad(ItemGroup extends Item> parent, String name) throws IOException {
this.parent = parent;
@@ -407,7 +425,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
public String getShortUrl() {
return getParent().getUrlChildPrefix()+'/'+Util.rawEncode(getName())+'/';
}
-
+
public String getSearchUrl() {
return getShortUrl();
}
diff --git a/core/src/main/java/hudson/model/Item.java b/core/src/main/java/hudson/model/Item.java
index b9092facfa2920a2637b678479c4ed0e4afeab4c..003c9e1a19ec25b7241aed5c85ec4ee128cdfecb 100644
--- a/core/src/main/java/hudson/model/Item.java
+++ b/core/src/main/java/hudson/model/Item.java
@@ -124,6 +124,14 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
* of the ancestors.
*/
String getFullDisplayName();
+
+ /**
+ * Gets the relative display name to this item from the specified group
+ *
+ * @since 1.512
+ * @return
+ */
+ String getRelativeDisplayNameFrom(ItemGroup g);
/**
* Gets the relative name to this item from the specified group.
@@ -159,7 +167,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
* URL that ends with '/'.
*/
String getShortUrl();
-
+
/**
* Returns the absolute URL of this item. This relies on the current
* {@link StaplerRequest} to figure out what the host name is,
diff --git a/core/src/main/java/hudson/model/Items.java b/core/src/main/java/hudson/model/Items.java
index f58306ebc1b4bb795181ec0aa29b8c6308291539..e4cb36b69287779f3a0eead534461146e8c9a51f 100644
--- a/core/src/main/java/hudson/model/Items.java
+++ b/core/src/main/java/hudson/model/Items.java
@@ -235,6 +235,31 @@ public class Items {
public static XmlFile getConfigFile(Item item) {
return getConfigFile(item.getRootDir());
}
+
+ /**
+ * Gets all the {@link Item}s recursively in the {@link ItemGroup} tree
+ * and filter them by the given type.
+ */
+ public static List getAllItems(ItemGroup root, Class type) {
+ List r = new ArrayList();
+
+ Stack q = new Stack();
+ q.push(root);
+
+ while(!q.isEmpty()) {
+ ItemGroup> parent = q.pop();
+ for (Item i : parent.getItems()) {
+ if(type.isInstance(i)) {
+ if (i.hasPermission(Item.READ))
+ r.add(type.cast(i));
+ }
+ if(i instanceof ItemGroup)
+ q.push((ItemGroup)i);
+ }
+ }
+
+ return r;
+ }
/**
* Used to load/save job configuration.
diff --git a/core/src/main/java/hudson/model/ListView.java b/core/src/main/java/hudson/model/ListView.java
index cda589ee47a926bfd1b3dc0c337639a3ef1031aa..f406582ca07315afa96c6c12f72b9455e349fa01 100644
--- a/core/src/main/java/hudson/model/ListView.java
+++ b/core/src/main/java/hudson/model/ListView.java
@@ -33,24 +33,29 @@ import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.views.ListViewColumn;
import hudson.views.ViewJobFilter;
-import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.HttpResponse;
-import org.kohsuke.stapler.QueryParameter;
-import org.kohsuke.stapler.StaplerRequest;
-import org.kohsuke.stapler.StaplerResponse;
-import org.kohsuke.stapler.interceptor.RequirePOST;
-import javax.annotation.concurrent.GuardedBy;
-import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SortedSet;
+import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
+import javax.annotation.concurrent.GuardedBy;
+import javax.servlet.ServletException;
+
+import net.sf.json.JSONObject;
+
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.HttpResponse;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.kohsuke.stapler.interceptor.RequirePOST;
+
/**
* Displays {@link Job}s in a flat list view.
*
@@ -63,16 +68,25 @@ public class ListView extends View implements Saveable {
*/
@GuardedBy("this")
/*package*/ final SortedSet jobNames = new TreeSet(CaseInsensitiveComparator.INSTANCE);
+
+ @GuardedBy("this")
+ private transient SortedSet allJobNames;
private DescribableList> jobFilters;
private DescribableList> columns;
+
/**
* Include regex string.
*/
private String includeRegex;
+ /**
+ * Whether to recurse in ItemGroups
+ */
+ private boolean recurse = true;
+
/**
* Compiled include pattern from the includeRegex string.
*/
@@ -138,33 +152,31 @@ public class ListView extends View implements Saveable {
*/
public List getItems() {
SortedSet names;
+ List items = new ArrayList();
+ SortedSet allNames;
synchronized (this) {
names = new TreeSet(jobNames);
+ allNames = allJobNames = new TreeSet(CaseInsensitiveComparator.INSTANCE);
}
- if (includePattern != null) {
- for (Item item : getOwnerItemGroup().getItems()) {
- String itemName = item.getName();
- if (includePattern.matcher(itemName).matches()) {
- names.add(itemName);
- }
- }
- }
+ ItemGroup extends TopLevelItem> parent = getOwnerItemGroup();
+ includeItems(parent, names);
Boolean statusFilter = this.statusFilter; // capture the value to isolate us from concurrent update
- List items = new ArrayList(names.size());
- for (TopLevelItem item : getOwnerItemGroup().getItems()) {
- if (!names.contains(item.getName())) continue;
+ for (TopLevelItem item : Items.getAllItems(getOwnerItemGroup(), TopLevelItem.class)) {
+ if (!names.contains(item.getRelativeNameFrom(getOwnerItemGroup()))) continue;
// Add if no status filter or filter matches enabled/disabled status:
if(statusFilter == null || !(item instanceof AbstractProject)
- || ((AbstractProject)item).isDisabled() ^ statusFilter)
+ || ((AbstractProject)item).isDisabled() ^ statusFilter) {
items.add(item);
+ allNames.add(item.getName());
+ }
}
// check the filters
Iterable jobFilters = getJobFilters();
- List allItems = new ArrayList(getOwnerItemGroup().getItems());
+ List allItems = new ArrayList(parent.getItems());
for (ViewJobFilter jobFilter: jobFilters) {
items = jobFilter.filter(items, allItems, this);
}
@@ -173,9 +185,51 @@ public class ListView extends View implements Saveable {
return items;
}
+
+ private TopLevelItem getItem(ItemGroup extends TopLevelItem> parent, String name) {
+ if (!name.contains("/")) { // same namespace
+ return parent.getItem(name);
+ }
+ StringTokenizer stringTokenizer = new StringTokenizer(name, "/");
+ TopLevelItem leaf = null;
+ ItemGroup extends TopLevelItem> node = parent;
+ // Navigate down to the item using / as separator
+ while(stringTokenizer.hasMoreTokens()) {
+ leaf = node.getItem(stringTokenizer.nextToken());
+ if (stringTokenizer.hasMoreTokens()) {
+ if (leaf instanceof ItemGroup) {
+ node = (ItemGroup extends TopLevelItem>)leaf;
+ } else {
+ return null;
+ }
+ }
+ }
+ return leaf;
+ }
+
+ private void includeItems(ItemGroup extends TopLevelItem> parent, SortedSet names) {
+ includeItems(parent, parent, names);
+ }
+
+ private void includeItems(ItemGroup extends TopLevelItem> root, ItemGroup extends TopLevelItem> parent, SortedSet names) {
+ if (includePattern != null) {
+ for (Item item : parent.getItems()) {
+ if (recurse && item instanceof ItemGroup) {
+ includeItems(root, (ItemGroup extends TopLevelItem>)item, names);
+ }
+ String itemName = item.getRelativeNameFrom(root);
+ if (includePattern.matcher(itemName).matches()) {
+ names.add(itemName);
+ }
+ }
+ }
+ }
public synchronized boolean contains(TopLevelItem item) {
- return jobNames.contains(item.getName());
+ if(allJobNames == null) {
+ getItems();
+ }
+ return allJobNames.contains(item.getName());
}
/**
@@ -193,6 +247,10 @@ public class ListView extends View implements Saveable {
public String getIncludeRegex() {
return includeRegex;
}
+
+ public boolean isRecurse() {
+ return recurse;
+ }
/**
* Filter by enabled/disabled status of jobs.
@@ -257,6 +315,7 @@ public class ListView extends View implements Saveable {
*/
@Override
protected void submit(StaplerRequest req) throws ServletException, FormException, IOException {
+ JSONObject json = req.getSubmittedForm();
synchronized (this) {
jobNames.clear();
for (TopLevelItem item : getOwnerItemGroup().getItems()) {
@@ -275,16 +334,17 @@ public class ListView extends View implements Saveable {
includeRegex = null;
includePattern = null;
}
+ recurse = json.optBoolean("recurse", true);
if (columns == null) {
columns = new DescribableList>(this);
}
- columns.rebuildHetero(req, req.getSubmittedForm(), ListViewColumn.all(), "columns");
+ columns.rebuildHetero(req, json, ListViewColumn.all(), "columns");
if (jobFilters == null) {
jobFilters = new DescribableList>(this);
}
- jobFilters.rebuildHetero(req, req.getSubmittedForm(), ViewJobFilter.all(), "jobFilters");
+ jobFilters.rebuildHetero(req, json, ViewJobFilter.all(), "jobFilters");
String filter = Util.fixEmpty(req.getParameter("statusFilter"));
statusFilter = filter != null ? "1".equals(filter) : null;
diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java
index e5241ba8f14cb7d731da0299502ebc175c49a333..42e61eaf5a2551c0c22e499763dc702dc3cd8243 100755
--- a/core/src/main/java/jenkins/model/Jenkins.java
+++ b/core/src/main/java/jenkins/model/Jenkins.java
@@ -1385,24 +1385,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
* and filter them by the given type.
*/
public List getAllItems(Class type) {
- List r = new ArrayList();
-
- Stack q = new Stack();
- q.push(this);
-
- while(!q.isEmpty()) {
- ItemGroup> parent = q.pop();
- for (Item i : parent.getItems()) {
- if(type.isInstance(i)) {
- if (i.hasPermission(Item.READ))
- r.add(type.cast(i));
- }
- if(i instanceof ItemGroup)
- q.push((ItemGroup)i);
- }
- }
-
- return r;
+ return Items.getAllItems(this, type);
}
/**
diff --git a/core/src/main/resources/hudson/model/ListView/configure-entries.jelly b/core/src/main/resources/hudson/model/ListView/configure-entries.jelly
index ceb721adfc3a8b0908c5bb827b0fb3e2f7cdd363..26e59290d4e467010ead140543722faf624148c6 100644
--- a/core/src/main/resources/hudson/model/ListView/configure-entries.jelly
+++ b/core/src/main/resources/hudson/model/ListView/configure-entries.jelly
@@ -40,17 +40,20 @@ THE SOFTWARE.
-
+
+ checked="${it.includeRegex != null}" help="/help/view-config/includeregex.html" inline="true">
+
+
+
diff --git a/core/src/main/resources/hudson/model/View/main.groovy b/core/src/main/resources/hudson/model/View/main.groovy
index 8f51aab6f96d25910aaff56267b1761bbd8c1d19..fdf1b73fced3b635cb6f6a3fb0958f9b6aafcc04 100644
--- a/core/src/main/resources/hudson/model/View/main.groovy
+++ b/core/src/main/resources/hudson/model/View/main.groovy
@@ -11,7 +11,7 @@ if (items.isEmpty()) {
}
include(my,"noJob.jelly");
} else {
- t.projectView(jobs: items, jobBaseUrl: "", showViewTabs: true, columnExtensions: my.columns, indenter: my.indenter) {
+ t.projectView(jobs: items, jobBaseUrl: "", showViewTabs: true, columnExtensions: my.columns, indenter: my.indenter, useFullName: true) {
set("views",my.owner.views);
set("currentView",my);
if (my.owner.class == hudson.model.MyViewsProperty.class) {
diff --git a/core/src/main/resources/hudson/views/JobColumn/column.jelly b/core/src/main/resources/hudson/views/JobColumn/column.jelly
index 4d30d67d596e4b357defab1bcfd907f29ca36ec0..1b6bbe17ae2df01a63d39b2b37fd4327a9714eaa 100644
--- a/core/src/main/resources/hudson/views/JobColumn/column.jelly
+++ b/core/src/main/resources/hudson/views/JobColumn/column.jelly
@@ -25,6 +25,6 @@ THE SOFTWARE.
- ${useFullName ? job.fullDisplayName : job.displayName}
+ ${useFullName ? job.getRelativeDisplayNameFrom(currentView.owner.itemGroup) : job.displayName}
|
\ No newline at end of file
diff --git a/core/src/main/resources/lib/hudson/projectView.jelly b/core/src/main/resources/lib/hudson/projectView.jelly
index 895329966c3ee156feae7860f68142b465ad05e2..fb257a70ab353cca8b8ef1ef896e6324cd8ff6d0 100644
--- a/core/src/main/resources/lib/hudson/projectView.jelly
+++ b/core/src/main/resources/lib/hudson/projectView.jelly
@@ -79,7 +79,7 @@ THE SOFTWARE.
-
+