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)