diff --git a/src/Hydra/lib/Hydra/Controller/Root.pm b/src/Hydra/lib/Hydra/Controller/Root.pm
index 69488af2..b4683b8f 100644
--- a/src/Hydra/lib/Hydra/Controller/Root.pm
+++ b/src/Hydra/lib/Hydra/Controller/Root.pm
@@ -41,7 +41,7 @@ sub trim {
 
 sub getBuild {
     my ($c, $id) = @_;
-    (my $build) = $c->model('DB::Builds')->search({ id => $id });
+    my $build = $c->model('DB::Builds')->find($id);
     return $build;
 }
 
@@ -168,6 +168,84 @@ sub all :Local {
 }
 
 
+sub releasesets :Local {
+    my ($self, $c, $projectName) = @_;
+    $c->stash->{template} = 'releasesets.tt';
+
+    my $project = $c->model('DB::Projects')->find($projectName);
+    return error($c, "Project $projectName doesn't exist.") if !defined $project;
+    $c->stash->{curProject} = $project;
+
+    $c->stash->{releaseSets} = [$project->releasesets->all];
+}
+
+
+sub releases :Local {
+    my ($self, $c, $projectName, $releaseName) = @_;
+    $c->stash->{template} = 'releases.tt';
+
+    my $project = $c->model('DB::Projects')->find($projectName);
+    return error($c, "Project $projectName doesn't exist.") if !defined $project;
+    $c->stash->{curProject} = $project;
+
+    (my $releaseSet) = $c->model('DB::Releasesets')->find($projectName, $releaseName);
+    return error($c, "Release set $releaseName doesn't exist.") if !defined $releaseSet;
+    $c->stash->{releaseSet} = $releaseSet;
+
+    (my $primaryJob) = $releaseSet->releasesetjobs->search({isprimary => 1});
+    return error($c, "Release set $releaseName doesn't have a primary job.") if !defined $primaryJob;
+
+    $c->stash->{jobs} = [$releaseSet->releasesetjobs->search({}, {order_by => "isprimary DESC"})];
+    
+    my @primaryBuilds = $project->builds->search(
+        { attrname => $primaryJob->job, finished => 1 },
+        { join => 'resultInfo', order_by => "timestamp DESC", '+select' => ["resultInfo.releasename"], '+as' => ["releasename"] });
+
+    my @releases = ();
+
+    foreach my $primaryBuild (@primaryBuilds) {
+        my @jobs = ();
+
+        my $status = 0; # = okay
+        
+        foreach my $job (@{$c->stash->{jobs}}) {
+            my $thisBuild;
+            
+            if ($job->isprimary == 1) {
+                $thisBuild = $primaryBuild;
+            } else {
+                # Find a build of this job that had the primary build
+                # as input.  If there are multiple, prefer successful
+                # ones, and then oldest.  !!! order_by buildstatus is hacky
+                ($thisBuild) = $primaryBuild->dependentBuilds->search(
+                    { attrname => $job->job, finished => 1 },
+                    { join => 'resultInfo', rows => 1
+                    , order_by => ["buildstatus", "timestamp"] });
+            }
+
+            if ($job->mayfail != 1) {
+                if (!defined $thisBuild) {
+                    $status = 2 if $status == 0; # = unfinished
+                } elsif ($thisBuild->resultInfo->buildstatus != 0) {
+                    $status = 1; # = failed
+                }
+            }
+            
+            push @jobs, { build => $thisBuild };
+        }
+        
+        push @releases,
+            { id => $primaryBuild->id
+            , releasename => $primaryBuild->get_column('releasename')
+            , jobs => [@jobs]
+            , status => $status
+            };
+    }
+
+    $c->stash->{releases} = [@releases];
+}
+
+
 sub updateProject {
     my ($c, $project) = @_;
     my $projectName = trim $c->request->params->{name};
@@ -296,7 +374,7 @@ sub project :Local {
     my ($self, $c, $projectName, $subcommand, $arg) = @_;
     $c->stash->{template} = 'project.tt';
     
-    (my $project) = $c->model('DB::Projects')->search({ name => $projectName });
+    my $project = $c->model('DB::Projects')->find($projectName);
     return error($c, "Project $projectName doesn't exist.") if !defined $project;
 
     my $isPosted = $c->request->method eq "POST";
@@ -386,7 +464,7 @@ sub job :Local {
     my ($self, $c, $projectName, $jobName) = @_;
     $c->stash->{template} = 'job.tt';
 
-    (my $project) = $c->model('DB::Projects')->search({ name => $projectName });
+    my $project = $c->model('DB::Projects')->find($projectName);
     return error($c, "Project $projectName doesn't exist.") if !defined $project;
     $c->stash->{curProject} = $project;
 
diff --git a/src/Hydra/lib/Hydra/Schema.pm b/src/Hydra/lib/Hydra/Schema.pm
index 2e9c9e93..abd1e599 100644
--- a/src/Hydra/lib/Hydra/Schema.pm
+++ b/src/Hydra/lib/Hydra/Schema.pm
@@ -8,8 +8,8 @@ use base 'DBIx::Class::Schema';
 __PACKAGE__->load_classes;
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:yXQEjv8/1aoKNW095xSR/Q
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:jJnmW70e1RDsSt5ClahomQ
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Buildinputs.pm b/src/Hydra/lib/Hydra/Schema/Buildinputs.pm
index 8d9cf578..917c3a69 100644
--- a/src/Hydra/lib/Hydra/Schema/Buildinputs.pm
+++ b/src/Hydra/lib/Hydra/Schema/Buildinputs.pm
@@ -36,8 +36,8 @@ __PACKAGE__->belongs_to("build", "Hydra::Schema::Builds", { id => "build" });
 __PACKAGE__->belongs_to("dependency", "Hydra::Schema::Builds", { id => "dependency" });
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:uaNcxZMTbF9WDLgf2G1Klw
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:R1F2JbVygktvK55xmY8mcg
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Buildproducts.pm b/src/Hydra/lib/Hydra/Schema/Buildproducts.pm
index d13e110b..2cf8cd89 100644
--- a/src/Hydra/lib/Hydra/Schema/Buildproducts.pm
+++ b/src/Hydra/lib/Hydra/Schema/Buildproducts.pm
@@ -33,8 +33,8 @@ __PACKAGE__->set_primary_key("build", "productnr");
 __PACKAGE__->belongs_to("build", "Hydra::Schema::Builds", { id => "build" });
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:btk6BJGE0Hj9qTO4qChpfw
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:aZuZd+oUAO1c8GvSbgn7Fw
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Buildresultinfo.pm b/src/Hydra/lib/Hydra/Schema/Buildresultinfo.pm
index 91f5f395..09544b03 100644
--- a/src/Hydra/lib/Hydra/Schema/Buildresultinfo.pm
+++ b/src/Hydra/lib/Hydra/Schema/Buildresultinfo.pm
@@ -29,8 +29,8 @@ __PACKAGE__->set_primary_key("id");
 __PACKAGE__->belongs_to("id", "Hydra::Schema::Builds", { id => "id" });
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Cn7vCpqfbTiq1/JF48BG2Q
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:QahlwGdZKC7mL7fvwNxWjA
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Builds.pm b/src/Hydra/lib/Hydra/Schema/Builds.pm
index fc0810aa..32261994 100644
--- a/src/Hydra/lib/Hydra/Schema/Builds.pm
+++ b/src/Hydra/lib/Hydra/Schema/Builds.pm
@@ -70,11 +70,13 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:p67v2RE44sAk2yGFoTpPww
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:uRSa4YkaRG0K6vK/qhGI9w
 
 __PACKAGE__->has_many(dependents => 'Hydra::Schema::Buildinputs', 'dependency');
 
+__PACKAGE__->many_to_many(dependentBuilds => 'dependents', 'build');
+
 __PACKAGE__->has_many(inputs => 'Hydra::Schema::Buildinputs', 'build');
 
 __PACKAGE__->belongs_to(
diff --git a/src/Hydra/lib/Hydra/Schema/Buildschedulinginfo.pm b/src/Hydra/lib/Hydra/Schema/Buildschedulinginfo.pm
index 6e94a233..1b896310 100644
--- a/src/Hydra/lib/Hydra/Schema/Buildschedulinginfo.pm
+++ b/src/Hydra/lib/Hydra/Schema/Buildschedulinginfo.pm
@@ -25,8 +25,8 @@ __PACKAGE__->set_primary_key("id");
 __PACKAGE__->belongs_to("id", "Hydra::Schema::Builds", { id => "id" });
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hdFMzqZ1IIdypz+/KLoCIw
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xBocoeipFdRsWDhvtoXImA
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Buildsteps.pm b/src/Hydra/lib/Hydra/Schema/Buildsteps.pm
index da375d37..5ac2afc1 100644
--- a/src/Hydra/lib/Hydra/Schema/Buildsteps.pm
+++ b/src/Hydra/lib/Hydra/Schema/Buildsteps.pm
@@ -35,8 +35,8 @@ __PACKAGE__->set_primary_key("id", "stepnr");
 __PACKAGE__->belongs_to("id", "Hydra::Schema::Builds", { id => "id" });
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zFljaYEbDkYbHuCmcIJhOA
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:04BankpQ6xo6T/ioMTdWkQ
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Cachedpathinputs.pm b/src/Hydra/lib/Hydra/Schema/Cachedpathinputs.pm
index 717e99a3..ada7c572 100644
--- a/src/Hydra/lib/Hydra/Schema/Cachedpathinputs.pm
+++ b/src/Hydra/lib/Hydra/Schema/Cachedpathinputs.pm
@@ -22,8 +22,8 @@ __PACKAGE__->add_columns(
 __PACKAGE__->set_primary_key("srcpath", "sha256hash");
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:E9++anIBM/+OIi2UdhIZKA
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Nq3TpcRmpSRWNL4Q1hGGrA
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Cachedsubversioninputs.pm b/src/Hydra/lib/Hydra/Schema/Cachedsubversioninputs.pm
index 291ce375..776ace25 100644
--- a/src/Hydra/lib/Hydra/Schema/Cachedsubversioninputs.pm
+++ b/src/Hydra/lib/Hydra/Schema/Cachedsubversioninputs.pm
@@ -20,8 +20,8 @@ __PACKAGE__->add_columns(
 __PACKAGE__->set_primary_key("uri", "revision");
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:eKcfAgBW789dI2VFGh4baw
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CCbHomM+8BTBqHBeGOGcuA
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Jobsetinputalts.pm b/src/Hydra/lib/Hydra/Schema/Jobsetinputalts.pm
index a3de2fd1..1c2dddaa 100644
--- a/src/Hydra/lib/Hydra/Schema/Jobsetinputalts.pm
+++ b/src/Hydra/lib/Hydra/Schema/Jobsetinputalts.pm
@@ -33,8 +33,8 @@ __PACKAGE__->belongs_to(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:vEw8HtMT848S/GEL1Y1MUg
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JPf4ozBKK6NQPJT2few40g
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Jobsetinputs.pm b/src/Hydra/lib/Hydra/Schema/Jobsetinputs.pm
index 2bfeccd7..53abdc06 100644
--- a/src/Hydra/lib/Hydra/Schema/Jobsetinputs.pm
+++ b/src/Hydra/lib/Hydra/Schema/Jobsetinputs.pm
@@ -43,8 +43,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JVmtu+NXI6P/GD5q7+YTDA
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:S8z1W0kjUX9VN5HPjyGAzA
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Jobsets.pm b/src/Hydra/lib/Hydra/Schema/Jobsets.pm
index 6ace1bf5..1573305d 100644
--- a/src/Hydra/lib/Hydra/Schema/Jobsets.pm
+++ b/src/Hydra/lib/Hydra/Schema/Jobsets.pm
@@ -50,8 +50,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:e1BZx0WYj1b6iIov6KvCqA
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ebblUCTW7I1wGhVlPfNd3Q
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Projects.pm b/src/Hydra/lib/Hydra/Schema/Projects.pm
index ee7a8766..450d30a1 100644
--- a/src/Hydra/lib/Hydra/Schema/Projects.pm
+++ b/src/Hydra/lib/Hydra/Schema/Projects.pm
@@ -30,10 +30,20 @@ __PACKAGE__->has_many(
   "Hydra::Schema::Jobsets",
   { "foreign.project" => "self.name" },
 );
+__PACKAGE__->has_many(
+  "releasesets",
+  "Hydra::Schema::Releasesets",
+  { "foreign.project" => "self.name" },
+);
+__PACKAGE__->has_many(
+  "releasesetjobs",
+  "Hydra::Schema::Releasesetjobs",
+  { "foreign.project" => "self.name" },
+);
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BHYbrizctvmbAJyTKSu89g
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:70/Br6966ZZ+p8n6lF1hcw
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Releasesetjobs.pm b/src/Hydra/lib/Hydra/Schema/Releasesetjobs.pm
new file mode 100644
index 00000000..875ff915
--- /dev/null
+++ b/src/Hydra/lib/Hydra/Schema/Releasesetjobs.pm
@@ -0,0 +1,40 @@
+package Hydra::Schema::Releasesetjobs;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class';
+
+__PACKAGE__->load_components("Core");
+__PACKAGE__->table("ReleaseSetJobs");
+__PACKAGE__->add_columns(
+  "project",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "release",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "job",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "attrs",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "isprimary",
+  { data_type => "integer", is_nullable => 0, size => undef },
+  "mayfail",
+  { data_type => "integer", is_nullable => 0, size => undef },
+  "description",
+  { data_type => "text", is_nullable => 0, size => undef },
+);
+__PACKAGE__->set_primary_key("project", "release", "job", "attrs");
+__PACKAGE__->belongs_to("project", "Hydra::Schema::Projects", { name => "project" });
+__PACKAGE__->belongs_to(
+  "releaseset",
+  "Hydra::Schema::Releasesets",
+  { name => "release", project => "project" },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:t2ZI1kBn/GsKlY0e4+Wspg
+
+
+# You can replace this text with custom content, and it will be preserved on regeneration
+1;
diff --git a/src/Hydra/lib/Hydra/Schema/Releasesets.pm b/src/Hydra/lib/Hydra/Schema/Releasesets.pm
new file mode 100644
index 00000000..cd6876f8
--- /dev/null
+++ b/src/Hydra/lib/Hydra/Schema/Releasesets.pm
@@ -0,0 +1,37 @@
+package Hydra::Schema::Releasesets;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class';
+
+__PACKAGE__->load_components("Core");
+__PACKAGE__->table("ReleaseSets");
+__PACKAGE__->add_columns(
+  "project",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "name",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "description",
+  { data_type => "text", is_nullable => 0, size => undef },
+  "keep",
+  { data_type => "integer", is_nullable => 0, size => undef },
+);
+__PACKAGE__->set_primary_key("project", "name");
+__PACKAGE__->belongs_to("project", "Hydra::Schema::Projects", { name => "project" });
+__PACKAGE__->has_many(
+  "releasesetjobs",
+  "Hydra::Schema::Releasesetjobs",
+  {
+    "foreign.project" => "self.project",
+    "foreign.release" => "self.name",
+  },
+);
+
+
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:pNqwNlXuENM/SsZ/utKhWw
+
+
+# You can replace this text with custom content, and it will be preserved on regeneration
+1;
diff --git a/src/Hydra/lib/Hydra/Schema/Systemtypes.pm b/src/Hydra/lib/Hydra/Schema/Systemtypes.pm
index 300668ee..fc122ed0 100644
--- a/src/Hydra/lib/Hydra/Schema/Systemtypes.pm
+++ b/src/Hydra/lib/Hydra/Schema/Systemtypes.pm
@@ -16,8 +16,8 @@ __PACKAGE__->add_columns(
 __PACKAGE__->set_primary_key("system");
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:90X5M27CbmJcZ7YnciHVMA
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WeoKp84cptljEdtD+5l7Ug
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Userroles.pm b/src/Hydra/lib/Hydra/Schema/Userroles.pm
index e5e6f3a7..3fab2beb 100644
--- a/src/Hydra/lib/Hydra/Schema/Userroles.pm
+++ b/src/Hydra/lib/Hydra/Schema/Userroles.pm
@@ -17,8 +17,8 @@ __PACKAGE__->set_primary_key("username", "role");
 __PACKAGE__->belongs_to("username", "Hydra::Schema::Users", { username => "username" });
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:g2EVNE74pSi9teIFqIA92Q
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:WxjgPLWPvXpQ3nmxmlU7Dw
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/lib/Hydra/Schema/Users.pm b/src/Hydra/lib/Hydra/Schema/Users.pm
index 36cadf77..366409a5 100644
--- a/src/Hydra/lib/Hydra/Schema/Users.pm
+++ b/src/Hydra/lib/Hydra/Schema/Users.pm
@@ -25,8 +25,8 @@ __PACKAGE__->has_many(
 );
 
 
-# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 03:26:23
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gmqkPkkET+452wBlILgOsQ
+# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-27 14:48:09
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:s+M14nuDVIMoRSgXodj3dw
 
 
 # You can replace this text with custom content, and it will be preserved on regeneration
diff --git a/src/Hydra/programs/Build.pl b/src/Hydra/programs/Build.pl
index f398cbb8..fc4e2b14 100644
--- a/src/Hydra/programs/Build.pl
+++ b/src/Hydra/programs/Build.pl
@@ -276,7 +276,7 @@ print STDERR "performing build $buildId\n";
 # have the lock taken away.
 my $build;
 $db->txn_do(sub {
-    ($build) = $db->resultset('Builds')->search({id => $buildId});
+    $build = $db->resultset('Builds')->find($buildId);
     die "build $buildId doesn't exist" unless defined $build;
     if ($build->schedulingInfo->busy != 0 && $build->schedulingInfo->locker != getppid) {
         die "build $buildId is already being built";
diff --git a/src/Hydra/root/jobstatus.tt b/src/Hydra/root/jobstatus.tt
index 214d2ce1..03152601 100644
--- a/src/Hydra/root/jobstatus.tt
+++ b/src/Hydra/root/jobstatus.tt
@@ -1,7 +1,7 @@
 [% WRAPPER layout.tt title="Job Status" %]
 [% PROCESS common.tt %]
 
-<h1>Job Status[% IF curProject %] in Project <tt>[% curProject.name %]</tt>[% END %]</h1>
+<h1>Job Status[% IF curProject %] of Project <tt>[% curProject.name %]</tt>[% END %]</h1>
 
 <p>Below are the latest builds for each job.</p>
 
diff --git a/src/Hydra/root/layout.tt b/src/Hydra/root/layout.tt
index abc80ca4..ace544d5 100644
--- a/src/Hydra/root/layout.tt
+++ b/src/Hydra/root/layout.tt
@@ -99,9 +99,9 @@
                   <div class="title"><a href="[% c.uri_for('/project' project.name) %]">[% HTML.escape(project.displayname) %]</a></div>
                   [% IF curProject.name == project.name %]
                     <ul class="subsubmenu">
-                      [% INCLUDE makeLink uri = c.uri_for('/project' project.name 'edit') title = "Edit" %]
                       [% INCLUDE makeLink uri = c.uri_for('/project' project.name 'jobstatus') title = "Job status" %]
                       [% INCLUDE makeLink uri = c.uri_for('/project' project.name 'all') title = "All builds" %]
+                      [% INCLUDE makeLink uri = c.uri_for('/project' project.name 'edit') title = "Edit" %]
                     </ul>
                   [% END %]
                 </li>
diff --git a/src/Hydra/root/project.tt b/src/Hydra/root/project.tt
index 6c170a88..3fd2d69f 100644
--- a/src/Hydra/root/project.tt
+++ b/src/Hydra/root/project.tt
@@ -96,7 +96,11 @@
       <tr>
         <th>Last checked:</th>
         <td>
-          [% PROCESS renderDateTime timestamp = jobset.lastcheckedtime %]
+          [% IF jobset.lastcheckedtime %]
+            [% PROCESS renderDateTime timestamp = jobset.lastcheckedtime %]
+          [% ELSE %]
+            <em>never</em>
+          [% END %]
         </td>
       </tr>
     [% END %]
diff --git a/src/Hydra/root/releases.tt b/src/Hydra/root/releases.tt
new file mode 100644
index 00000000..5a13e1f5
--- /dev/null
+++ b/src/Hydra/root/releases.tt
@@ -0,0 +1,60 @@
+[% WRAPPER layout.tt title="Releases" %]
+[% PROCESS common.tt %]
+[% USE HTML %]
+
+<h1>Releases</h1>
+
+<!-- <p>Description: [% releaseSet.description %]</p> -->
+
+<table class="tablesorter">
+  <thead>
+    <tr>
+      <th></th>
+      <th>#</th>
+      <th>Release</th>
+      [% FOREACH job IN jobs %]
+        <th>[% IF job.description; HTML.escape(job.description); ELSE %]<tt>[% job.job %]</tt> ([% job.attrs %])[% END %]</th>
+      [% END %]
+    </tr>
+  </thead>
+  
+  <tbody>
+    [% FOREACH release IN releases %] 
+      <tr>
+        <td>
+          [% IF release.status == 0 %]
+            <img src="/static/images/success.gif" />
+          [% ELSIF release.status == 1 %]
+            <img src="/static/images/failure.gif" />
+          [% ELSIF release.status == 2 %]
+            <img src="/static/images/question-mark.png" />
+          [% END %]
+        </td>
+        <td>[% release.id %]</td>
+        <td>
+          [% IF release.releasename %]
+            <tt>[% release.releasename %]</tt>
+          [% ELSE %]
+            <em>No name</em>
+          [% END %]
+        </td>
+        [% FOREACH job IN release.jobs %]
+          <td>
+            [% IF job.build %]
+              <a href="[% c.uri_for('/build' job.build.id) %]">
+                [% IF job.build.resultInfo.buildstatus == 0 %]
+                  <img src="/static/images/success.gif" />
+                [% ELSE %]
+                  <img src="/static/images/failure.gif" />
+                [% END %]
+                [% job.build.id %]
+              </a>
+            [% END %]
+          </td>
+        [% END %]
+      </tr> 
+   [% END %]
+  </tbody>
+</table>
+
+[% END %]
diff --git a/src/Hydra/root/releasesets.tt b/src/Hydra/root/releasesets.tt
new file mode 100644
index 00000000..03a93c3d
--- /dev/null
+++ b/src/Hydra/root/releasesets.tt
@@ -0,0 +1,14 @@
+[% WRAPPER layout.tt title="Release Sets" %]
+[% PROCESS common.tt %]
+
+<h1>Release Sets</h1>
+
+<p>Project <tt>[% curProject.name %]</tt> has the following release sets:</p>
+
+<ul>
+  [% FOREACH releaseSet IN releaseSets %]
+    <li><a href="[% c.uri_for('/releases' curProject.name releaseSet.name) %]"><tt>[% releaseSet.name %]</tt></a></li>
+  [% END %]
+</ul>
+
+[% END %]
diff --git a/src/Hydra/root/static/images/question-mark.png b/src/Hydra/root/static/images/question-mark.png
new file mode 100644
index 00000000..4c20e5a2
Binary files /dev/null and b/src/Hydra/root/static/images/question-mark.png differ
diff --git a/src/hydra.sql b/src/hydra.sql
index e1673af1..0463e38b 100644
--- a/src/hydra.sql
+++ b/src/hydra.sql
@@ -168,6 +168,8 @@ create trigger cascadeProjectUpdate
     update JobsetInputs set project = new.name where project = old.name;
     update JobsetInputAlts set project = new.name where project = old.name;
     update Builds set project = new.name where project = old.name;
+    update ReleaseSets set project = new.name where project = old.name;
+    update ReleaseSetJobs set project = new.name where project = old.name;
   end;
 
 
@@ -288,3 +290,64 @@ create trigger cascadeUserDelete
   for each row begin
     delete from UserRoles where userName = old.userName;
   end;
+
+
+-- Release sets are a mechanism to automatically group related builds
+-- together.  A release set defines what an individual release
+-- consists of, namely: a release consists of a build of some
+-- "primary" job, plus all builds of the other jobs named in
+-- ReleaseSetJobs that have that build as an input.  If there are
+-- multiple builds matching a ReleaseSetJob, then we take the *oldest*
+-- successful build (for release stability), or the *newest*
+-- unsuccessful build if there is no succesful build.  A release is
+-- itself considered successful if all builds (except those for jobs
+-- that have mayFail set) are successful.
+--
+-- Note that individual releases aren't separately stored in the
+-- database, so they're really just a dynamic view on the universe of
+-- builds, defined by a ReleaseSet.
+create table ReleaseSets (
+    project       text not null,
+    name          text not null,
+    
+    description   text,
+
+    -- If true, don't garbage-collect builds belonging to the releases
+    -- defined by this row.
+    keep          integer not null default 0, 
+
+    primary key   (project, name),
+    foreign key   (project) references Projects(name) on delete cascade -- ignored by sqlite
+);
+
+
+create trigger cascadeReleaseSetDelete
+  before delete on ReleaseSets
+  for each row begin
+    delete from ReleaseSetJobs where project = old.project and release = old.release;
+  end;
+
+
+create table ReleaseSetJobs (
+    project       text not null,
+    release       text not null,
+
+    job           text not null,
+
+    -- A constraint on the job consisting of `name=value' pairs,
+    -- e.g. "system=i686-linux officialRelease=true".  Should really
+    -- be a separate table but I'm lazy.
+    attrs         text not null,
+
+    -- If set, this is the primary job for the release.  There can be
+    -- onlyt one such job per release set.
+    isPrimary     integer not null default 0,
+    
+    mayFail       integer not null default 0,
+
+    description   text,
+    
+    primary key   (project, release, job, attrs),
+    foreign key   (project) references Projects(name) on delete cascade, -- ignored by sqlite
+    foreign key   (project, release) references ReleaseSets(project, name) on delete cascade -- ignored by sqlite
+);