diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj
index 9fc4be1489..fdc5726edf 100644
--- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj
+++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj
@@ -114,6 +114,7 @@
+
diff --git a/src/NuGetGallery.Core/Packaging/InvalidPackageException.cs b/src/NuGetGallery.Core/Packaging/InvalidPackageException.cs
new file mode 100644
index 0000000000..0e5c10ef3f
--- /dev/null
+++ b/src/NuGetGallery.Core/Packaging/InvalidPackageException.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NuGetGallery.Packaging
+{
+ [Serializable]
+ public class InvalidPackageException : Exception
+ {
+ public InvalidPackageException() { }
+ public InvalidPackageException(string message) : base(message) { }
+ public InvalidPackageException(string message, Exception inner) : base(message, inner) { }
+ protected InvalidPackageException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+}
diff --git a/src/NuGetGallery.Core/Packaging/Nupkg.cs b/src/NuGetGallery.Core/Packaging/Nupkg.cs
index 948c58a90e..ed70e34fa8 100644
--- a/src/NuGetGallery.Core/Packaging/Nupkg.cs
+++ b/src/NuGetGallery.Core/Packaging/Nupkg.cs
@@ -112,13 +112,13 @@ public static Manifest SafelyLoadManifest(Stream stream, bool leaveOpen)
private static Manifest SafelyLoadManifest(ZipArchive archive)
{
var manifestEntry = archive.Entries.SingleOrDefault(entry =>
- entry.Name.IndexOf("/", StringComparison.Ordinal) == -1
+ entry.FullName.IndexOf("/", StringComparison.Ordinal) == -1
&& entry.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)
);
if (manifestEntry == null)
{
- throw new InvalidOperationException("The package does not contain a manifest.");
+ throw new InvalidPackageException("A manifest was not found at the root of the package.");
}
using (var safeStream = GetSizeVerifiedFileStream(manifestEntry, MaxManifestSize))
diff --git a/src/NuGetGallery/App_Code/ViewHelpers.cshtml b/src/NuGetGallery/App_Code/ViewHelpers.cshtml
index ceb16a6e08..87012a92a6 100644
--- a/src/NuGetGallery/App_Code/ViewHelpers.cshtml
+++ b/src/NuGetGallery/App_Code/ViewHelpers.cshtml
@@ -129,9 +129,10 @@
string brand = config == null ? "" : config.Current.Brand;
- This is the @brand.
+ This is the @brand
@if (ver.Present)
{
+ @:, version @ver.Version.
if(!String.IsNullOrEmpty(ver.ShortCommit)) {
Deployed from
@@ -155,8 +156,10 @@
}
if(ver.BuildDateUtc != DateTime.MinValue) {
- @: Built at @ver.BuildDateUtc.ToNuGetShortDateString().
+ @: Built at @ver.BuildDateUtc.ToNuGetShortDateString().
}
+ } else {
+ @:.
}
@* A little quick-n-dirty code to display the current machine *@
diff --git a/src/NuGetGallery/App_Start/AppActivator.cs b/src/NuGetGallery/App_Start/AppActivator.cs
index 0dd7898e49..cc4d169d68 100644
--- a/src/NuGetGallery/App_Start/AppActivator.cs
+++ b/src/NuGetGallery/App_Start/AppActivator.cs
@@ -131,7 +131,7 @@ private static void BundlingPostStart()
.Include("~/Scripts/jquery-{version}.js")
.Include("~/Scripts/jquery.validate.js")
.Include("~/Scripts/jquery.validate.unobtrusive.js")
- .Include("~/Scripts/typeahead.bundle.js")
+ .Include("~/Scripts/jquery.timeago.js")
.Include("~/Scripts/nugetgallery.js")
.Include("~/Scripts/stats.js");
BundleTable.Bundles.Add(scriptBundle);
diff --git a/src/NuGetGallery/App_Start/Routes.cs b/src/NuGetGallery/App_Start/Routes.cs
index 7728c6e6e4..7e165e42b8 100644
--- a/src/NuGetGallery/App_Start/Routes.cs
+++ b/src/NuGetGallery/App_Start/Routes.cs
@@ -315,12 +315,12 @@ public static void RegisterRoutes(RouteCollection routes)
routes.MapRoute(
"v2PackageIds",
"api/v2/package-ids",
- new { controller = "Api", action = "GetPackageIds" });
+ new { controller = "Api", action = "PackageIDs" });
routes.MapRoute(
"v2PackageVersions",
"api/v2/package-versions/{id}",
- new { controller = "Api", action = "GetPackageVersions" });
+ new { controller = "Api", action = "PackageVersions" });
routes.MapRoute(
RouteName.StatisticsDownloadsApi,
diff --git a/src/NuGetGallery/Content/Layout.css b/src/NuGetGallery/Content/Layout.css
index 34335eaeb9..70efc67f7e 100644
--- a/src/NuGetGallery/Content/Layout.css
+++ b/src/NuGetGallery/Content/Layout.css
@@ -3,8 +3,7 @@
/* Service Alert */
-#service-alert
-{
+#service-alert {
display: none;
}
@@ -35,15 +34,16 @@
background-image: linear-gradient(to bottom, #ff0000, #e60000);
}
-.banner-urgent a
-{
- color: white;
- text-decoration: underline;
-}
+ .banner-urgent a {
+ color: white;
+ text-decoration: underline;
+ }
/* Header */
-header.main { height: 95px; }
+header.main {
+ height: 95px;
+}
/* Site Logo */
@@ -52,15 +52,15 @@ header.main { height: 95px; }
padding: 20px 20px 0 0;
}
-#logo a {
- background: url(../Content/Logos/nugetlogo.png) no-repeat;
- display: block;
- height: 75px;
- margin: 0;
- padding: 0;
- text-indent: -9999px;
- width: 225px;
-}
+ #logo a {
+ background: url(../Content/Logos/nugetlogo.png) no-repeat;
+ display: block;
+ height: 75px;
+ margin: 0;
+ padding: 0;
+ text-indent: -9999px;
+ width: 225px;
+ }
/* Top Menu (Navigation) */
@@ -71,50 +71,49 @@ nav.main {
margin-top: 10px;
}
-nav.main ul {
- margin: 0px;
- padding: 0px;
-}
-
-nav.main ul li {
- display: block;
- float: left;
- height: 42px;
-}
-
-nav.main ul li.current a {
- background-color: #e4f1f7;
- border: 1px solid #ebf2f5;
- border-bottom: none;
- border-top: 1px solid #fff;
- color: #195670;
- height: 46px;
- line-height: 44px;
- position: relative;
- text-decoration: none;
- top: -4px;
-}
-
-nav.main ul a {
- -moz-border-radius: 4px 4px 0 0;
- -webkit-border-radius: 4px 4px 0 0;
- /*CSS3 properties*/
- border-radius: 4px 4px 0 0;
- color: #fff;
- display: block;
- font-size: 1.2em;
- height: 42px;
- line-height: 44px;
-
- margin: 0 5px;
- padding: 0 10px;
- text-decoration: none;
-}
-
-nav.main ul a:hover {
- background-color: #3c3d44;
- text-decoration: none;
-}
+ nav.main ul {
+ margin: 0px;
+ padding: 0px;
+ }
+
+ nav.main ul li {
+ display: block;
+ float: left;
+ height: 42px;
+ }
+
+ nav.main ul li.current a {
+ background-color: #e4f1f7;
+ border: 1px solid #ebf2f5;
+ border-bottom: none;
+ border-top: 1px solid #fff;
+ color: #195670;
+ height: 46px;
+ line-height: 44px;
+ position: relative;
+ text-decoration: none;
+ top: -4px;
+ }
+
+ nav.main ul a {
+ -moz-border-radius: 4px 4px 0 0;
+ -webkit-border-radius: 4px 4px 0 0;
+ /*CSS3 properties*/
+ border-radius: 4px 4px 0 0;
+ color: #fff;
+ display: block;
+ font-size: 1.2em;
+ height: 42px;
+ line-height: 44px;
+ margin: 0 5px;
+ padding: 0 10px;
+ text-decoration: none;
+ }
+
+ nav.main ul a:hover {
+ background-color: #3c3d44;
+ text-decoration: none;
+ }
/* User Display (Sign-In Info) and search box */
@@ -131,12 +130,14 @@ nav.main ul a:hover {
margin-top: 6px;
}
-.user-display a {
- color: #fff;
- font-weight: 600;
-}
+ .user-display a {
+ color: #fff;
+ font-weight: 600;
+ }
-.user-display a:hover { text-decoration: underline; }
+ .user-display a:hover {
+ text-decoration: underline;
+ }
#searchBox {
background-color: #fff;
@@ -144,14 +145,14 @@ nav.main ul a:hover {
/*CSS3 properties*/
height: 32px;
margin-top: 1px;
- width: 704px;
-
+ width: 690px;
padding-left: 5px;
padding-right: 4px;
vertical-align: top;
}
#searchBoxInput {
+ width: 650px;
border: 0px;
color: #333;
font-size: 20px;
@@ -159,7 +160,6 @@ nav.main ul a:hover {
line-height: 30px;
outline: none;
padding: 0;
- width: 673px;
}
#searchBoxSubmit {
@@ -173,7 +173,6 @@ nav.main ul a:hover {
margin-left: 0;
box-shadow: none;
margin-top: 1px;
-
text-indent: -9999px;
vertical-align: bottom;
width: 27px;
@@ -192,7 +191,9 @@ nav.main ul a:hover {
/* Footer */
-.clear-fix { clear: both; }
+.clear-fix {
+ clear: both;
+}
#layout-footer {
background: #e4f1f7;
@@ -210,44 +211,50 @@ footer#footer {
display: table;
}
-footer#footer a { color: #3e483c; }
+ footer#footer a {
+ color: #3e483c;
+ }
-footer#footer a:hover {
- text-decoration: underline;
-}
+ footer#footer a:hover {
+ text-decoration: underline;
+ }
-footer#footer p {
- margin: 0;
- padding: 0;
-}
+ footer#footer p {
+ margin: 0;
+ padding: 0;
+ }
-footer#footer p#releaseTag { margin: 10px 0 0 0; }
+ footer#footer p#releaseTag {
+ margin: 10px 0 0 0;
+ }
-footer#footer ul.recommended {
- list-style: none;
- margin: 0 auto;
- padding: 0;
-}
+ footer#footer ul.recommended {
+ list-style: none;
+ margin: 0 auto;
+ padding: 0;
+ }
-footer#footer ul.recommended li {
- float: left;
- margin-left: 0;
- padding: 10px;
- text-align: left;
- width: 170px;
-}
+ footer#footer ul.recommended li {
+ float: left;
+ margin-left: 0;
+ padding: 10px;
+ text-align: left;
+ width: 170px;
+ }
-footer#footer ul.recommended li a {
- display: block;
- font-size: 1.3em;
-}
+ footer#footer ul.recommended li a {
+ display: block;
+ font-size: 1.3em;
+ }
-footer#footer ul.recommended li p { font-size: .9em; }
+ footer#footer ul.recommended li p {
+ font-size: .9em;
+ }
-footer#footer div.license {
- clear: both;
- font-size: .7em;
-}
+ footer#footer div.license {
+ clear: both;
+ font-size: .7em;
+ }
/* Error Layout (with background image)*/
@@ -285,4 +292,4 @@ footer#footer div.license {
float: right;
margin-bottom: 20px;
width: 75%;
-}
\ No newline at end of file
+}
diff --git a/src/NuGetGallery/Content/Site.css b/src/NuGetGallery/Content/Site.css
index 78e77706e7..44e31cd690 100644
--- a/src/NuGetGallery/Content/Site.css
+++ b/src/NuGetGallery/Content/Site.css
@@ -79,11 +79,11 @@ h6 {
padding-bottom: 10px;
}
-.page-heading h1,
-.page-heading h2 {
- margin: 0;
- padding: 0;
-}
+ .page-heading h1,
+ .page-heading h2 {
+ margin: 0;
+ padding: 0;
+ }
/* Aside Headings */
@@ -226,15 +226,15 @@ nav, section {
padding: 20px 20px 0 0;
}
-#logo a {
- background: url(../Content/Images/nugetlogo.png) no-repeat;
- display: block;
- height: 75px;
- margin: 0;
- padding: 0;
- text-indent: -9999px;
- width: 345px;
-}
+ #logo a {
+ background: url(../Content/Images/nugetlogo.png) no-repeat;
+ display: block;
+ height: 75px;
+ margin: 0;
+ padding: 0;
+ text-indent: -9999px;
+ width: 345px;
+ }
/* Body */
@@ -248,9 +248,9 @@ nav, section {
width: 25%;
}
- #pageSearchBox #searchBoxInput {
- width: 265px;
- }
+#pageSearchBox #searchBoxInput {
+ width: 265px;
+}
/* Logo */
@@ -301,7 +301,9 @@ ul.owners {
padding: 0;
}
-ul.owners li { margin-bottom: 12px; }
+ ul.owners li {
+ margin-bottom: 12px;
+ }
a.owner {
font-size: 1.4em;
@@ -313,16 +315,20 @@ a.owner {
top: -0.5em;
}
-a.owner:hover {
- text-decoration: none;
- color: #333;
-}
+ a.owner:hover {
+ text-decoration: none;
+ color: #333;
+ }
-.owner-image { margin-right: 5px; }
+.owner-image {
+ margin-right: 5px;
+}
/* Authors */
-p.authors { font-size: 1.25em; }
+p.authors {
+ font-size: 1.25em;
+}
/* Large Package Icon - up to 128x128 */
@@ -436,7 +442,7 @@ p.authors { font-size: 1.25em; }
}
#sideColumn nav ul li {
- margin: 0;
+ margin: 3px 0px 0px;
padding: 0;
}
@@ -465,6 +471,7 @@ fieldset.search {
}
#searchResults {
+ clear: both;
list-style: none;
margin: 0;
padding: 0;
@@ -476,6 +483,12 @@ fieldset.search {
/* List Package */
+.package-list-lastupdated {
+ float: left;
+ font-style: italic;
+ font-size: 8pt;
+}
+
section.package {
border-top: 1px solid #ccc;
padding-top: 10px;
@@ -513,7 +526,9 @@ section.package {
}
section.package ul,
- section.package li { display: inline; }
+ section.package li {
+ display: inline;
+ }
section.package h1 {
font-size: 1.75em;
@@ -591,7 +606,9 @@ ul.pager {
padding-right: 7px;
}
-ul.pager li.next { padding-left: 10px; }
+ ul.pager li.next {
+ padding-left: 10px;
+ }
/* Sexy Table */
@@ -637,9 +654,9 @@ ul.pager li.next { padding-left: 10px; }
background-color: #f4f5f6;
}
-.sexy-table tbody tr.recommended {
- font-weight: 800;
-}
+ .sexy-table tbody tr.recommended {
+ font-weight: 800;
+ }
.sexy-table tbody td {
padding: 5px 25px 5px 0;
@@ -860,9 +877,13 @@ fieldset.form {
display: none;
}
-.form-field p { margin-left: 10px; }
+.form-field p {
+ margin-left: 10px;
+}
-.form-field h3 { color: #52a4ca; }
+.form-field h3 {
+ color: #52a4ca;
+}
.form-field {
margin-bottom: 10px;
@@ -914,16 +935,21 @@ fieldset.form {
padding-left: 6px;
}
-.form-field input[type="checkbox"] { border-left: none; }
+ .form-field input[type="checkbox"] {
+ border-left: none;
+ }
-.form-field input[type="url"] { width: 100%; }
+ .form-field input[type="url"] {
+ width: 100%;
+ }
+
+ .form-field select {
+ color: #7f8c7d;
+ font-size: 1.25em;
+ padding: 2px;
+ margin: 0px 0px 10px 0;
+ }
-.form-field select {
- color: #7f8c7d;
- font-size: 1.25em;
- padding: 2px;
- margin: 0px 0px 10px 0;
-}
.form-field select[data-edited=true] {
border-left: solid 4px #2ef12e;
padding-left: 0px;
@@ -945,31 +971,31 @@ fieldset.form {
background-color: #e6e6e6;
}
-.form-field select[data-edited=true] {
- border-left: solid 4px #2ef12e;
- padding-left: 0px;
-}
+ .form-field select[data-edited=true] {
+ border-left: solid 4px #2ef12e;
+ padding-left: 0px;
+ }
-.form-field textarea[disabled],
-.form-field input[type="email"][disabled],
-.form-field input[type="text"][disabled],
-.form-field input[type="file"][disabled],
-.form-field input[type="password"][disabled],
-.form input[type="submit"][disabled],
-.form input[type="submit"][disabled]:hover {
- background: silver;
- cursor: not-allowed;
- border: none;
- opacity: 0.65;
- filter: alpha(opacity=65);
- color: #333;
- background-color: #e6e6e6;
-}
+ .form-field textarea[disabled],
+ .form-field input[type="email"][disabled],
+ .form-field input[type="text"][disabled],
+ .form-field input[type="file"][disabled],
+ .form-field input[type="password"][disabled],
+ .form input[type="submit"][disabled],
+ .form input[type="submit"][disabled]:hover {
+ background: silver;
+ cursor: not-allowed;
+ border: none;
+ opacity: 0.65;
+ filter: alpha(opacity=65);
+ color: #333;
+ background-color: #e6e6e6;
+ }
-.form-field input.input-validation-error,
-.form-field textarea.input-validation-error {
- border-left: solid 5px #ca5252;
-}
+ .form-field input.input-validation-error,
+ .form-field textarea.input-validation-error {
+ border-left: solid 5px #ca5252;
+ }
/* flyout Text */
@@ -1030,6 +1056,10 @@ a.btn {
box-sizing: border-box;
}
+.btn.btn-small {
+ font-size: 10pt;
+}
+
.btn.btn-inline {
display: inline-block;
}
@@ -1109,8 +1139,7 @@ button, input[type="submit"], .btn {
}
/* Last Updated Timestamp (in various places)*/
-.last-updated
-{
+.last-updated {
margin-top: 1em;
clear: both;
color: grey;
@@ -1126,36 +1155,36 @@ button, input[type="submit"], .btn {
padding: 0;
}
-.sequence li {
- border: solid 1px #333;
- box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 1), 1px 1px 1px rgba(0, 0, 0, 0.3);
- color: #333;
- float: left;
- height: 35px;
- line-height: 35px;
- margin: 0 30px 20px 0;
- padding: 0 20px;
- width: auto;
-}
+ .sequence li {
+ border: solid 1px #333;
+ box-shadow: inset 0px 0px 1px rgba(255, 255, 255, 1), 1px 1px 1px rgba(0, 0, 0, 0.3);
+ color: #333;
+ float: left;
+ height: 35px;
+ line-height: 35px;
+ margin: 0 30px 20px 0;
+ padding: 0 20px;
+ width: auto;
+ }
-.sequence li.past,
-.sequence li.current {
- background-color: #4585aa;
- background-image: -ms-linear-gradient(top, #4585aa 0%, #376783 100%);
- background-image: -o-linear-gradient(top, #4585aa 0%, #376783 100%);
- background-image: -webkit-linear-gradient(top, #4585aa 0%, #376783 100%);
- background-image: linear-gradient(top, #4585aa 0%, #376783 100%);
- border-color: #376783;
- color: #fff;
-}
+ .sequence li.past,
+ .sequence li.current {
+ background-color: #4585aa;
+ background-image: -ms-linear-gradient(top, #4585aa 0%, #376783 100%);
+ background-image: -o-linear-gradient(top, #4585aa 0%, #376783 100%);
+ background-image: -webkit-linear-gradient(top, #4585aa 0%, #376783 100%);
+ background-image: linear-gradient(top, #4585aa 0%, #376783 100%);
+ border-color: #376783;
+ color: #fff;
+ }
-.sequence li.current {
- font-weight: 600;
- height: 41px;
- line-height: 41px;
- margin-top: -3px;
- text-decoration: underline;
-}
+ .sequence li.current {
+ font-weight: 600;
+ height: 41px;
+ line-height: 41px;
+ margin-top: -3px;
+ text-decoration: underline;
+ }
.btn.btn-big {
width: 100%;
@@ -1211,13 +1240,13 @@ ul.actionlist {
color: #0071bc;
}
- ul.actionlist li.actionlist-item a.actionlist-item-link:hover .actionlist-item-header .actionlist-item-header-text {
- text-decoration: underline;
- }
+ ul.actionlist li.actionlist-item a.actionlist-item-link:hover .actionlist-item-header .actionlist-item-header-text {
+ text-decoration: underline;
+ }
- ul.actionlist li.actionlist-item a.actionlist-item-link:hover {
- text-decoration: none
- }
+ ul.actionlist li.actionlist-item a.actionlist-item-link:hover {
+ text-decoration: none;
+ }
ul.accordian {
margin: 0;
@@ -1248,6 +1277,7 @@ ul.accordian {
font-size: 12pt;
margin-left: 1em;
}
+
ul.accordian li.accordian-item .accordian-item-subtitle .owner-image {
margin-bottom: -8px;
}
@@ -1276,6 +1306,10 @@ ul.accordian {
display: none;
}
+body.s-noclickonce .s-clickonce {
+ display: none;
+}
+
/* Icons */
.nucon-nuget-w {
background: url('images/icons/nuget_32_mono_w.png');
@@ -1302,11 +1336,10 @@ ul.accordian {
}
span.sorted-by {
- width: 100%;
- text-align: right;
+ float: right;
color: #52a4ca;
display: block;
font-size: 1.25em;
margin: 0.5em;
margin-left: 0;
-}
\ No newline at end of file
+}
diff --git a/src/NuGetGallery/Controllers/CuratedFeedsController.cs b/src/NuGetGallery/Controllers/CuratedFeedsController.cs
index b4f5f21070..77a077a886 100644
--- a/src/NuGetGallery/Controllers/CuratedFeedsController.cs
+++ b/src/NuGetGallery/Controllers/CuratedFeedsController.cs
@@ -81,6 +81,7 @@ public virtual async Task ListPackages(string curatedFeedName, str
var viewModel = new PackageListViewModel(
results.Data,
+ results.IndexTimestampUtc,
q,
totalHits,
page - 1,
diff --git a/src/NuGetGallery/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs
index ed27399c2f..f01e089561 100644
--- a/src/NuGetGallery/Controllers/PackagesController.cs
+++ b/src/NuGetGallery/Controllers/PackagesController.cs
@@ -190,9 +190,15 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadF
{
nuGetPackage = CreatePackage(uploadStream);
}
+ catch (InvalidPackageException ipex)
+ {
+ ipex.Log();
+ ModelState.AddModelError(String.Empty, ipex.Message);
+ return View();
+ }
catch (Exception ex)
{
- QuietLog.LogHandledException(ex);
+ ex.Log();
ModelState.AddModelError(String.Empty, Strings.FailedToReadUploadFile);
return View();
}
@@ -303,6 +309,7 @@ public virtual async Task ListPackages(string q, int page = 1)
var viewModel = new PackageListViewModel(
results.Data,
+ results.IndexTimestampUtc,
q,
totalHits,
page - 1,
@@ -768,19 +775,13 @@ public virtual async Task VerifyPackage()
return RedirectToRoute(RouteName.UploadPackage);
}
- try
+ using (INupkg package = await SafeCreatePackage(currentUser, uploadFile))
{
- using (INupkg package = CreatePackage(uploadFile))
+ if (package == null)
{
- packageMetadata = package.Metadata;
+ return Redirect(Url.UploadPackage());
}
- }
- catch (InvalidDataException e)
- {
- // Log the exception in case we get support requests about it.
- QuietLog.LogHandledException(e);
-
- return View("UnverifiablePackage");
+ packageMetadata = package.Metadata;
}
}
@@ -826,7 +827,13 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formD
return new RedirectResult(Url.UploadPackage());
}
- INupkg nugetPackage = CreatePackage(uploadFile);
+ INupkg nugetPackage = await SafeCreatePackage(currentUser, uploadFile);
+ if (nugetPackage == null)
+ {
+ // Send the user back
+ return new RedirectResult(Url.UploadPackage());
+ }
+ Debug.Assert(nugetPackage != null);
// Rule out problem scenario with multiple tabs - verification request (possibly with edits) was submitted by user
// viewing a different package to what was actually most recently uploaded
@@ -903,6 +910,35 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formD
return RedirectToRoute(RouteName.DisplayPackage, new { package.PackageRegistration.Id, package.Version });
}
+ private async Task SafeCreatePackage(NuGetGallery.User currentUser, Stream uploadFile)
+ {
+ Exception caught = null;
+ INupkg nugetPackage = null;
+ try
+ {
+ nugetPackage = CreatePackage(uploadFile);
+ }
+ catch (InvalidPackageException ipex)
+ {
+ caught = ipex.AsUserSafeException();
+ }
+ catch (Exception ex)
+ {
+ // Can't wait for Roslyn to let us await in Catch blocks :(
+ caught = ex;
+ }
+ if (caught != null)
+ {
+ caught.Log();
+ // Report the error
+ TempData["Message"] = caught.GetUserSafeMessage();
+
+ // Clear the upload
+ await _uploadFileService.DeleteUploadFileAsync(currentUser.Key);
+ }
+ return nugetPackage;
+ }
+
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
@@ -950,7 +986,15 @@ internal virtual ActionResult SetLicenseReportVisibility(string id, string versi
// this methods exist to make unit testing easier
protected internal virtual INupkg CreatePackage(Stream stream)
{
- return new Nupkg(stream, leaveOpen: false);
+ try
+ {
+ return new Nupkg(stream, leaveOpen: false);
+ }
+ catch (Exception)
+ {
+ stream.Dispose();
+ throw;
+ }
}
private static string GetSortExpression(string sortOrder)
diff --git a/src/NuGetGallery/ExtensionMethods.cs b/src/NuGetGallery/ExtensionMethods.cs
index 248875469c..ce9f41565a 100644
--- a/src/NuGetGallery/ExtensionMethods.cs
+++ b/src/NuGetGallery/ExtensionMethods.cs
@@ -23,6 +23,11 @@ namespace NuGetGallery
{
public static class ExtensionMethods
{
+ public static string ToJavaScriptUTC(this DateTime self)
+ {
+ return self.ToUniversalTime().ToString("O", CultureInfo.CurrentCulture);
+ }
+
public static string ToNuGetShortDateTimeString(this DateTime self)
{
return self.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture);
@@ -395,4 +400,4 @@ private static User LoadUser(IOwinContext context)
return null; // No user logged in, or credentials could not be resolved
}
}
-}
\ No newline at end of file
+}
diff --git a/src/NuGetGallery/Infrastructure/ApplicationVersionHelper.cs b/src/NuGetGallery/Infrastructure/ApplicationVersionHelper.cs
index b1dbc68e5c..f3e86f2090 100644
--- a/src/NuGetGallery/Infrastructure/ApplicationVersionHelper.cs
+++ b/src/NuGetGallery/Infrastructure/ApplicationVersionHelper.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Web;
@@ -41,7 +42,7 @@ public ApplicationVersion(Uri repositoryBase, string version, string branch, str
Commit = commit;
BuildDateUtc = buildDateUtc;
- ShortCommit = String.IsNullOrEmpty(Commit) ? String.Empty : Commit.Substring(0, 10);
+ ShortCommit = String.IsNullOrEmpty(Commit) ? String.Empty : Commit.Substring(0, Math.Min(10, Commit.Length));
if (repositoryBase != null)
{
@@ -99,7 +100,7 @@ private static ApplicationVersion LoadVersion()
string repoUriString = TryGet(metadata, "RepositoryUrl");
DateTime buildDate;
- if (!DateTime.TryParse(dateString, out buildDate))
+ if (!DateTime.TryParse(dateString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out buildDate))
{
buildDate = DateTime.MinValue;
}
diff --git a/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs b/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs
index 9cd6a327c1..23b4773811 100644
--- a/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs
+++ b/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs
@@ -111,12 +111,13 @@ private async Task SearchCore(SearchFilter filter, bool raw)
var content = await result.ReadContent();
if (filter.CountOnly || content.TotalHits == 0)
{
- results = new SearchResults(content.TotalHits);
+ results = new SearchResults(content.TotalHits, content.IndexTimestamp);
}
else
{
results = new SearchResults(
content.TotalHits,
+ content.IndexTimestamp,
content.Data.Select(ReadPackage).AsQueryable());
}
}
diff --git a/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs b/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs
index fbf95c8cfa..ee1f84715b 100644
--- a/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs
+++ b/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Lucene.Net.Analysis;
@@ -49,6 +50,9 @@ public Task Search(SearchFilter searchFilter)
private SearchResults SearchCore(SearchFilter searchFilter)
{
+ // Get index timestamp
+ DateTime timestamp = File.GetLastWriteTimeUtc(LuceneCommon.GetIndexMetadataPath());
+
int numRecords = searchFilter.Skip + searchFilter.Take;
var searcher = new IndexSearcher(_directory, readOnly: true);
@@ -77,7 +81,7 @@ private SearchResults SearchCore(SearchFilter searchFilter)
if (results.TotalHits == 0 || searchFilter.CountOnly)
{
- return new SearchResults(results.TotalHits);
+ return new SearchResults(results.TotalHits, timestamp);
}
var packages = results.ScoreDocs
@@ -86,6 +90,7 @@ private SearchResults SearchCore(SearchFilter searchFilter)
.ToList();
return new SearchResults(
results.TotalHits,
+ timestamp,
packages.AsQueryable());
}
diff --git a/src/NuGetGallery/Infrastructure/UserSafeException.cs b/src/NuGetGallery/Infrastructure/UserSafeException.cs
new file mode 100644
index 0000000000..50262b889f
--- /dev/null
+++ b/src/NuGetGallery/Infrastructure/UserSafeException.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+
+namespace NuGetGallery
+{
+ // Marker interface to indicate that the exception has a message that can be shown to a user
+ public interface IUserSafeException
+ {
+ string UserMessage { get; }
+ Exception LoggedException { get; }
+ }
+
+ [Serializable]
+ public class UserSafeException : Exception, IUserSafeException
+ {
+ public string UserMessage
+ {
+ get { return Message; }
+ }
+
+ public Exception LoggedException
+ {
+ get { return InnerException == null ? this : InnerException; }
+ }
+
+ public UserSafeException() { }
+ public UserSafeException(string message) : base(message) { }
+ public UserSafeException(string message, Exception inner) : base(message, inner) { }
+ protected UserSafeException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context)
+ : base(info, context) { }
+ }
+
+ public static class UserSafeExceptionExtensions
+ {
+ public static UserSafeException AsUserSafeException(this Exception self)
+ {
+ return new UserSafeException(self.Message, self.InnerException);
+ }
+
+ public static string GetUserSafeMessage(this Exception self)
+ {
+ IUserSafeException uvex = self as IUserSafeException;
+ if (uvex != null)
+ {
+ return uvex.UserMessage;
+ }
+ return Strings.DefaultUserSafeExceptionMessage;
+ }
+
+ public static void Log(this Exception self)
+ {
+ IUserSafeException uvex = self as IUserSafeException;
+ if (uvex != null)
+ {
+ // Log the exception that the User-Visible wrapper marked as to-be-logged
+ QuietLog.LogHandledException(uvex.LoggedException);
+ }
+ else
+ {
+ QuietLog.LogHandledException(self);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj
index 9d7fa48edb..57b2b9e59c 100644
--- a/src/NuGetGallery/NuGetGallery.csproj
+++ b/src/NuGetGallery/NuGetGallery.csproj
@@ -271,13 +271,13 @@
False
..\..\packages\NuGet.Core.2.8.1\lib\net40-Client\NuGet.Core.dll
-
+
False
- ..\..\packages\NuGet.Services.Platform.Client.3.0.3-rel-0008\lib\portable-net45+wp80+win\NuGet.Services.Platform.Client.dll
+ ..\..\packages\NuGet.Services.Platform.Client.3.0.11-r-master\lib\portable-net45+wp80+win\NuGet.Services.Platform.Client.dll
-
+
False
- ..\..\packages\NuGet.Services.Search.Client.3.0.3-rel-0015\lib\portable-net45+wp80+win\NuGet.Services.Search.Client.dll
+ ..\..\packages\NuGet.Services.Search.Client.3.0.16-r-master\lib\portable-net45+wp80+win\NuGet.Services.Search.Client.dll
..\..\packages\ODataNullPropagationVisitor.0.5.4237.2641\lib\net40\ODataNullPropagationVisitor.dll
@@ -738,6 +738,7 @@
+
Code
@@ -1081,6 +1082,7 @@
+
diff --git a/src/NuGetGallery/Scripts/jquery.timeago.js b/src/NuGetGallery/Scripts/jquery.timeago.js
new file mode 100644
index 0000000000..91fd28ab0f
--- /dev/null
+++ b/src/NuGetGallery/Scripts/jquery.timeago.js
@@ -0,0 +1,214 @@
+/**
+ * Timeago is a jQuery plugin that makes it easy to support automatically
+ * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
+ *
+ * @name timeago
+ * @version 1.4.0
+ * @requires jQuery v1.2.3+
+ * @author Ryan McGeary
+ * @license MIT License - http://www.opensource.org/licenses/mit-license.php
+ *
+ * For usage and examples, visit:
+ * http://timeago.yarp.com/
+ *
+ * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
+ */
+
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else {
+ // Browser globals
+ factory(jQuery);
+ }
+}(function ($) {
+ $.timeago = function(timestamp) {
+ if (timestamp instanceof Date) {
+ return inWords(timestamp);
+ } else if (typeof timestamp === "string") {
+ return inWords($.timeago.parse(timestamp));
+ } else if (typeof timestamp === "number") {
+ return inWords(new Date(timestamp));
+ } else {
+ return inWords($.timeago.datetime(timestamp));
+ }
+ };
+ var $t = $.timeago;
+
+ $.extend($.timeago, {
+ settings: {
+ refreshMillis: 60000,
+ allowPast: true,
+ allowFuture: false,
+ localeTitle: false,
+ cutoff: 0,
+ strings: {
+ prefixAgo: null,
+ prefixFromNow: null,
+ suffixAgo: "ago",
+ suffixFromNow: "from now",
+ inPast: 'any moment now',
+ seconds: "less than a minute",
+ minute: "about a minute",
+ minutes: "%d minutes",
+ hour: "about an hour",
+ hours: "about %d hours",
+ day: "a day",
+ days: "%d days",
+ month: "about a month",
+ months: "%d months",
+ year: "about a year",
+ years: "%d years",
+ wordSeparator: " ",
+ numbers: []
+ }
+ },
+
+ inWords: function(distanceMillis) {
+ if(!this.settings.allowPast && ! this.settings.allowFuture) {
+ throw 'timeago allowPast and allowFuture settings can not both be set to false.';
+ }
+
+ var $l = this.settings.strings;
+ var prefix = $l.prefixAgo;
+ var suffix = $l.suffixAgo;
+ if (this.settings.allowFuture) {
+ if (distanceMillis < 0) {
+ prefix = $l.prefixFromNow;
+ suffix = $l.suffixFromNow;
+ }
+ }
+
+ if(!this.settings.allowPast && distanceMillis >= 0) {
+ return this.settings.strings.inPast;
+ }
+
+ var seconds = Math.abs(distanceMillis) / 1000;
+ var minutes = seconds / 60;
+ var hours = minutes / 60;
+ var days = hours / 24;
+ var years = days / 365;
+
+ function substitute(stringOrFunction, number) {
+ var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
+ var value = ($l.numbers && $l.numbers[number]) || number;
+ return string.replace(/%d/i, value);
+ }
+
+ var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
+ seconds < 90 && substitute($l.minute, 1) ||
+ minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
+ minutes < 90 && substitute($l.hour, 1) ||
+ hours < 24 && substitute($l.hours, Math.round(hours)) ||
+ hours < 42 && substitute($l.day, 1) ||
+ days < 30 && substitute($l.days, Math.round(days)) ||
+ days < 45 && substitute($l.month, 1) ||
+ days < 365 && substitute($l.months, Math.round(days / 30)) ||
+ years < 1.5 && substitute($l.year, 1) ||
+ substitute($l.years, Math.round(years));
+
+ var separator = $l.wordSeparator || "";
+ if ($l.wordSeparator === undefined) { separator = " "; }
+ return $.trim([prefix, words, suffix].join(separator));
+ },
+
+ parse: function(iso8601) {
+ var s = $.trim(iso8601);
+ s = s.replace(/\.\d+/,""); // remove milliseconds
+ s = s.replace(/-/,"/").replace(/-/,"/");
+ s = s.replace(/T/," ").replace(/Z/," UTC");
+ s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
+ s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
+ return new Date(s);
+ },
+ datetime: function(elem) {
+ var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
+ return $t.parse(iso8601);
+ },
+ isTime: function(elem) {
+ // jQuery's `is()` doesn't play well with HTML5 in IE
+ return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
+ }
+ });
+
+ // functions that can be called via $(el).timeago('action')
+ // init is default when no action is given
+ // functions are called with context of a single element
+ var functions = {
+ init: function(){
+ var refresh_el = $.proxy(refresh, this);
+ refresh_el();
+ var $s = $t.settings;
+ if ($s.refreshMillis > 0) {
+ this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
+ }
+ },
+ update: function(time){
+ var parsedTime = $t.parse(time);
+ $(this).data('timeago', { datetime: parsedTime });
+ if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
+ refresh.apply(this);
+ },
+ updateFromDOM: function(){
+ $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
+ refresh.apply(this);
+ },
+ dispose: function () {
+ if (this._timeagoInterval) {
+ window.clearInterval(this._timeagoInterval);
+ this._timeagoInterval = null;
+ }
+ }
+ };
+
+ $.fn.timeago = function(action, options) {
+ var fn = action ? functions[action] : functions.init;
+ if(!fn){
+ throw new Error("Unknown function name '"+ action +"' for timeago");
+ }
+ // each over objects here and call the requested function
+ this.each(function(){
+ fn.call(this, options);
+ });
+ return this;
+ };
+
+ function refresh() {
+ var data = prepareData(this);
+ var $s = $t.settings;
+
+ if (!isNaN(data.datetime)) {
+ if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) {
+ $(this).text(inWords(data.datetime));
+ }
+ }
+ return this;
+ }
+
+ function prepareData(element) {
+ element = $(element);
+ if (!element.data("timeago")) {
+ element.data("timeago", { datetime: $t.datetime(element) });
+ var text = $.trim(element.text());
+ if ($t.settings.localeTitle) {
+ element.attr("title", element.data('timeago').datetime.toLocaleString());
+ } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
+ element.attr("title", text);
+ }
+ }
+ return element.data("timeago");
+ }
+
+ function inWords(date) {
+ return $t.inWords(distance(date));
+ }
+
+ function distance(date) {
+ return (new Date().getTime() - date.getTime());
+ }
+
+ // fix for IE6 suckage
+ document.createElement("abbr");
+ document.createElement("time");
+}));
\ No newline at end of file
diff --git a/src/NuGetGallery/Scripts/nugetgallery.js b/src/NuGetGallery/Scripts/nugetgallery.js
index a3c4d3f057..87c1398f9c 100644
--- a/src/NuGetGallery/Scripts/nugetgallery.js
+++ b/src/NuGetGallery/Scripts/nugetgallery.js
@@ -26,6 +26,8 @@
checkServiceStatus();
attachPlugins();
+
+ sniffClickonce();
});
// Add validator that ensures provided value is NOT equal to a specified value.
@@ -43,6 +45,21 @@
return s;
}
+ function hasMimeTypeSupport(desiredMime) {
+ var mimes = window.navigator.mimeTypes,
+ hasSupport = false;
+
+ for (var i = 0; i < mimes.length; i++) {
+ var mime = mimes[i];
+
+ if (mime.type == desiredMime) {
+ hasSupport = true;
+ }
+ }
+
+ return hasSupport;
+ };
+
// Attach script plugins
function attachPlugins() {
$('.s-toggle[data-show][data-hide]').delegate('', 'click', function (evt) {
@@ -71,7 +88,7 @@
evt.preventDefault();
}
});
- if(!navigator.mimeTypes["application/x-shockwave-flash"]) {
+ if (!hasMimeTypeSupport("application/x-shockwave-flash")) {
$('.s-reqflash').remove();
}
$('.s-localtime[data-utc]').each(function () {
@@ -84,7 +101,17 @@
}
ampm = "PM";
}
- $(this).text(utc.getFullYear() + "-" + padInt(utc.getMonth(), 2) + "-" + padInt(utc.getDate(), 2) + " " + hrs + ":" + padInt(utc.getMinutes(), 2) + " " + ampm + " Local Time");
+ $(this).text(utc.getFullYear() + "-" + padInt(utc.getMonth() + 1, 2) + "-" + padInt(utc.getDate(), 2) + " " + hrs + ":" + padInt(utc.getMinutes(), 2) + " " + ampm + " Local Time");
});
+ $('time.timeago').timeago();
+ }
+
+ function sniffClickonce() {
+ var userAgent = window.navigator.userAgent.toUpperCase(),
+ hasNativeDotNet = userAgent.indexOf('.NET CLR 3.5') >= 0;
+
+ if (hasNativeDotNet) {
+ $('body').removeClass('s-noclickonce');
+ }
}
})(window, jQuery);
diff --git a/src/NuGetGallery/Services/ISearchService.cs b/src/NuGetGallery/Services/ISearchService.cs
index 0b117355aa..f6a19d4489 100644
--- a/src/NuGetGallery/Services/ISearchService.cs
+++ b/src/NuGetGallery/Services/ISearchService.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System;
+using System.Linq;
using System.Threading.Tasks;
namespace NuGetGallery
@@ -31,17 +32,19 @@ public interface IRawSearchService
public class SearchResults
{
public int Hits { get; private set; }
+ public DateTime? IndexTimestampUtc { get; private set; }
public IQueryable Data { get; private set; }
- public SearchResults(int hits)
- : this(hits, Enumerable.Empty().AsQueryable())
+ public SearchResults(int hits, DateTime? indexTimestampUtc)
+ : this(hits, indexTimestampUtc, Enumerable.Empty().AsQueryable())
{
}
- public SearchResults(int hits, IQueryable data)
+ public SearchResults(int hits, DateTime? indexTimestampUtc, IQueryable data)
{
Hits = hits;
Data = data;
+ IndexTimestampUtc = indexTimestampUtc;
}
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs
index a5238bbae1..c469179db0 100644
--- a/src/NuGetGallery/Strings.Designer.cs
+++ b/src/NuGetGallery/Strings.Designer.cs
@@ -1,7 +1,7 @@
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
-// Runtime Version:4.0.30319.34003
+// Runtime Version:4.0.30319.34014
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@@ -169,6 +169,15 @@ public static string DatabaseUnavailable_TrySpecificVersion {
}
}
+ ///
+ /// Looks up a localized string similar to An unexpected error occurred. Contact support for assistance..
+ ///
+ public static string DefaultUserSafeExceptionMessage {
+ get {
+ return ResourceManager.GetString("DefaultUserSafeExceptionMessage", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The email address '{0}' is being used..
///
diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx
index 395a8c0ee2..e119c5e783 100644
--- a/src/NuGetGallery/Strings.resx
+++ b/src/NuGetGallery/Strings.resx
@@ -299,4 +299,7 @@ The {2} Team
This package requires version '{0}' of NuGet, which this gallery does not currently support. Please contact us if you have questions.
+
+ An unexpected error occurred. Contact support for assistance.
+
\ No newline at end of file
diff --git a/src/NuGetGallery/UrlExtensions.cs b/src/NuGetGallery/UrlExtensions.cs
index e128c4271e..5eb8a4c40e 100644
--- a/src/NuGetGallery/UrlExtensions.cs
+++ b/src/NuGetGallery/UrlExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
@@ -7,6 +8,8 @@ namespace NuGetGallery
{
public static class UrlExtensions
{
+ private const string PackageExplorerDeepLink = @"https://npe.codeplex.com/releases/clickonce/NuGetPackageExplorer.application?url={0}&id={1}&version={2}";
+
// Shorthand for current url
public static string Current(this UrlHelper url)
{
@@ -140,9 +143,7 @@ public static string ExplorerDeepLink(this UrlHelper url, int feedVersion, strin
urlResult = EnsureTrailingSlash(urlResult);
- string explorerDeepLink = @"https://npe.codeplex.com/releases/clickonce/NuGetPackageExplorer.application?url={0}&id={1}&version={2}";
-
- return string.Format(explorerDeepLink, urlResult, id, version);
+ return String.Format(CultureInfo.InvariantCulture, PackageExplorerDeepLink, urlResult, id, version);
}
public static string LogOn(this UrlHelper url)
diff --git a/src/NuGetGallery/ViewModels/PackageListViewModel.cs b/src/NuGetGallery/ViewModels/PackageListViewModel.cs
index 75b73935d2..7e6a2ee843 100644
--- a/src/NuGetGallery/ViewModels/PackageListViewModel.cs
+++ b/src/NuGetGallery/ViewModels/PackageListViewModel.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
@@ -8,6 +9,7 @@ public class PackageListViewModel
{
public PackageListViewModel(
IQueryable packages,
+ DateTime? indexTimestampUtc,
string searchTerm,
int totalCount,
int pageIndex,
@@ -17,6 +19,7 @@ public PackageListViewModel(
// TODO: Implement actual sorting
IEnumerable items = packages.ToList().Select(pv => new ListPackageItemViewModel(pv));
PageIndex = pageIndex;
+ IndexTimestampUtc = indexTimestampUtc;
PageSize = pageSize;
TotalCount = totalCount;
SearchTerm = searchTerm;
@@ -49,5 +52,7 @@ public PackageListViewModel(
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
+
+ public DateTime? IndexTimestampUtc { get; private set; }
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml
index f0baf1c883..201d1f850a 100644
--- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml
+++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml
@@ -78,7 +78,7 @@
@if (User.Identity.IsAuthenticated)
{
Download
- Package Explorer
+ Open in Package Explorer
}
else
{
diff --git a/src/NuGetGallery/Views/Shared/Layout.cshtml b/src/NuGetGallery/Views/Shared/Layout.cshtml
index 885e8a9465..93e30d7909 100644
--- a/src/NuGetGallery/Views/Shared/Layout.cshtml
+++ b/src/NuGetGallery/Views/Shared/Layout.cshtml
@@ -20,7 +20,7 @@
@RenderSection("TopScripts", required: false)
@ViewHelpers.ReleaseMeta()
-
+
diff --git a/src/NuGetGallery/Views/Shared/ListPackages.cshtml b/src/NuGetGallery/Views/Shared/ListPackages.cshtml
index abb69c0384..fbc262fdc0 100644
--- a/src/NuGetGallery/Views/Shared/ListPackages.cshtml
+++ b/src/NuGetGallery/Views/Shared/ListPackages.cshtml
@@ -1,6 +1,7 @@
@model PackageListViewModel
@{
ViewBag.Title = String.IsNullOrWhiteSpace(Model.SearchTerm) ? "Packages" : "Packages matching " + Model.SearchTerm;
+ ViewBag.SortText = String.IsNullOrWhiteSpace(Model.SearchTerm) ? "recent installs" : "relevance";
ViewBag.Tab = "Packages";
}
@@ -27,14 +28,17 @@
There are @Model.TotalCount packages
}
}
- @if (@Model.LastResultIndex > 0)
+
Sorted by @ViewBag.SortText
+ @if (Model.LastResultIndex > 0)
{
Displaying results @Model.FirstResultIndex - @Model.LastResultIndex.
}
+ @if(Model.IndexTimestampUtc.HasValue)
+ {
+
Search Index last updated
+ }
-
Sorted by Recent Installs
-
@foreach (var package in Model.Items)
{
diff --git a/src/NuGetGallery/packages.config b/src/NuGetGallery/packages.config
index 1282e84f26..ae23b8484b 100644
--- a/src/NuGetGallery/packages.config
+++ b/src/NuGetGallery/packages.config
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs
index 28d3cc01ae..54216cfef8 100644
--- a/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs
+++ b/tests/NuGetGallery.Facts/Controllers/CuratedFeedsControllerFacts.cs
@@ -213,7 +213,7 @@ public async Task WillSearchForAPackage()
controller.StubSearchService
.Setup(stub => stub.Search(It.IsAny()))
- .Returns(Task.FromResult(new SearchResults(mockPackages.Count(), mockPackages)));
+ .Returns(Task.FromResult(new SearchResults(mockPackages.Count(), DateTime.UtcNow, mockPackages)));
var mockHttpContext = new Mock();
TestUtility.SetupHttpContextMockForUrlGeneration(mockHttpContext, controller);
diff --git a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs
index 37576305d7..d9da6bc5dd 100644
--- a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs
+++ b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs
@@ -108,7 +108,7 @@ private static Mock CreateSearchService()
{
var searchService = new Mock();
searchService.Setup(s => s.Search(It.IsAny())).Returns(
- (IQueryable p, string searchTerm) => Task.FromResult(new SearchResults(p.Count(), p)));
+ (IQueryable p, string searchTerm) => Task.FromResult(new SearchResults(p.Count(), DateTime.UtcNow, p)));
return searchService;
}
@@ -574,7 +574,7 @@ public async Task TrimsSearchTerm()
{
var searchService = new Mock();
searchService.Setup(s => s.Search(It.IsAny())).Returns(
- Task.FromResult(new SearchResults(0)));
+ Task.FromResult(new SearchResults(0, DateTime.UtcNow)));
var controller = CreateController(searchService: searchService);
controller.SetCurrentUser(TestUtility.FakeUser);
diff --git a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs
index 9a2d65a218..f93564645c 100644
--- a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs
+++ b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
@@ -83,7 +84,7 @@ public void V1FeedSearchDoesNotReturnPrereleasePackages()
configuration.Setup(c => c.GetSiteRoot(It.IsAny())).Returns("https://localhost:8081/");
var searchService = new Mock(MockBehavior.Strict);
searchService.Setup(s => s.Search(It.IsAny())).Returns
- , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), _)));
+ , string>((_, __) => Task.FromResult(new SearchResults(_.Count(), DateTime.UtcNow, _)));
searchService.Setup(s => s.ContainsAllVersions).Returns(false);
var v1Service = new TestableV1Feed(repo.Object, configuration.Object, searchService.Object);
diff --git a/tests/NuGetGallery.FunctionalTests.Fluent/TagSearchTest.cs b/tests/NuGetGallery.FunctionalTests.Fluent/TagSearchTest.cs
index 445e6114dc..c6d52df0d3 100644
--- a/tests/NuGetGallery.FunctionalTests.Fluent/TagSearchTest.cs
+++ b/tests/NuGetGallery.FunctionalTests.Fluent/TagSearchTest.cs
@@ -24,7 +24,10 @@ public void TagSearch()
string version = "1.0.0";
string tagString = ";This,is a,;test,,package, created ;by ,the NuGet;;;team.";
- UploadPackageIfNecessary(packageName, version, null, null, tagString, "This is a test package created by the NuGet team.");
+ if (CheckForPackageExistence)
+ {
+ UploadPackageIfNecessary(packageName, version, null, null, tagString, "This is a test package created by the NuGet team.");
+ }
// Go to the package page.
I.Open(UrlHelper.BaseUrl + @"Packages/" + packageName + "/" + version);
diff --git a/tests/NuGetGallery.FunctionalTests.vsmdi b/tests/NuGetGallery.FunctionalTests.vsmdi
index 3b9374bf7e..c2d6229821 100644
--- a/tests/NuGetGallery.FunctionalTests.vsmdi
+++ b/tests/NuGetGallery.FunctionalTests.vsmdi
@@ -17,14 +17,9 @@
-
-
-
-
-
@@ -37,11 +32,7 @@
-
-
-
-
diff --git a/tools/Get-BranchSummary.ps1 b/tools/Get-BranchSummary.ps1
new file mode 100644
index 0000000000..54128223bc
--- /dev/null
+++ b/tools/Get-BranchSummary.ps1
@@ -0,0 +1,22 @@
+param([DateTime]$Before)
+Write-Host "Calculating summary, this may take a few seconds..."
+git branch -r |
+ foreach { $_.Trim() } |
+ where { ($_ -notlike "origin/pr/*") -and ($_ -notlike "origin/HEAD*") } |
+ foreach { $_.Substring("origin/".Length) } |
+ foreach {
+ $log = (git log "origin/$_" -n1 --oneline)
+ $chunks = $log.Split(" ")
+ $commit = $chunks[0]
+ $comment = [String]::Join(" ", $chunks[1..($chunks.Length-1)])
+ $obj = New-Object PSCustomObject
+ Add-Member -InputObject $obj -NotePropertyMembers @{
+ "Name" = $_;
+ "Commit" = $commit;
+ "Comment" = $comment;
+ "Date" = [DateTime](@(git show -s --format=%ci $commit)[0]);
+ }
+ $obj
+ } |
+ sort Date |
+ where { (!$Before) -or ($_.Date -lt $Before) }
diff --git a/tools/Get-MergedBranches.ps1 b/tools/Get-MergedBranches.ps1
index 62b482aef9..f539e50c59 100644
--- a/tools/Get-MergedBranches.ps1
+++ b/tools/Get-MergedBranches.ps1
@@ -1,4 +1,4 @@
-param([string]$ParentBranch = "prod")
+param([string]$ParentBranch = "master")
git branch --merged "origin/$ParentBranch" -r |
foreach { $_.Trim() } |
@@ -6,7 +6,7 @@ git branch --merged "origin/$ParentBranch" -r |
where {
($_ -notlike "origin/pr*") -and
($_ -notlike "origin/HEAD*") -and
- (@("origin/master","origin/prod","origin/staging","origin/iter-start","origin/qa") -notcontains $_)
+ (@("origin/master") -notcontains $_)
} |
foreach {
$_.Substring("origin/".Length)