├── .gitignore ├── Export ├── App.config ├── ExcelHelper.cs ├── Export.csproj ├── Options.cs ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── GitMonitor.sln ├── LICENSE.md ├── README.md ├── Settings.StyleCop ├── packages └── Newtonsoft.Json.9.0.1 │ └── tools │ └── install.ps1 └── src └── GitMonitor ├── .bowerrc ├── Controllers ├── Api │ ├── GitBranchSearchController.cs │ ├── GitCommitController.cs │ ├── GitRepositoryController.cs │ └── GitSearchController.cs └── Web │ └── HomeController.cs ├── GitMonitor.csproj ├── Models ├── GitCommit.cs ├── GitRepository.cs ├── GitSearch.cs ├── MonitoredPath.cs └── MonitoredPathConfig.cs ├── Program.cs ├── Properties ├── PublishProfiles │ ├── FileSystem-publish.ps1 │ └── FileSystem.pubxml └── launchSettings.json ├── Repositories ├── CommitRepository.cs ├── Interfaces │ ├── ICommitRepository.cs │ └── IRepoRepository.cs └── RepoRepository.cs ├── Startup.cs ├── Views ├── Home │ └── Index.cshtml ├── Shared │ ├── Error.cshtml │ └── _Layout.cshtml ├── _ViewImports.cshtml └── _ViewStart.cshtml ├── app.config ├── appsettings.json ├── bower.json ├── gitmonitorconfig.json ├── gulpfile.js ├── package.json ├── web.config └── wwwroot ├── _references.js ├── css ├── site.css └── site.min.css ├── favicon.ico ├── js ├── site.js └── site.min.js └── lib └── bootstrap └── dist ├── css ├── bootstrap-grid.css ├── bootstrap-grid.css.map ├── bootstrap-grid.min.css ├── bootstrap-grid.min.css.map ├── bootstrap-reboot.css ├── bootstrap-reboot.css.map ├── bootstrap-reboot.min.css ├── bootstrap-reboot.min.css.map ├── bootstrap.css ├── bootstrap.css.map ├── bootstrap.min.css └── bootstrap.min.css.map └── js ├── bootstrap.bundle.js ├── bootstrap.bundle.min.js ├── bootstrap.js └── bootstrap.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | packages/ 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.sln.docstates 10 | *.sln.targets 11 | 12 | # Build results 13 | 14 | [Dd]ebug/ 15 | [Rr]elease/ 16 | x64/ 17 | build/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | *.generated.cs 21 | *.js.map 22 | *1?.sln 23 | _AllEdited.proj 24 | _lastlocalbuild.cmd 25 | packages 26 | 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | *_i.c 33 | *_p.c 34 | *.ilk 35 | *.meta 36 | *.obj 37 | *.pch 38 | *.pdb 39 | *.pgc 40 | *.pgd 41 | *.rsp 42 | *.sbr 43 | *.tlb 44 | *.tli 45 | *.tlh 46 | *.tmp 47 | *.tmp_proj 48 | *.log 49 | *.vspscc 50 | *.vssscc 51 | .builds 52 | *.pidb 53 | *.log 54 | *.scc 55 | 56 | # Visual C++ cache files 57 | ipch/ 58 | *.aps 59 | *.ncb 60 | *.opensdf 61 | *.sdf 62 | *.cachefile 63 | 64 | # Visual Studio profiler 65 | *.psess 66 | *.vsp 67 | *.vspx 68 | 69 | # Guidance Automation Toolkit 70 | *.gpState 71 | 72 | # ASP.NET 5 73 | *.lock.json 74 | .vs 75 | 76 | # ReSharper is a .NET coding add-in 77 | _ReSharper*/ 78 | *.[Rr]e[Ss]harper 79 | *.[Dd]ot[Ss]ettings 80 | 81 | # TeamCity is a build add-in 82 | _TeamCity* 83 | 84 | # DotCover is a Code Coverage Tool 85 | *.dotCover 86 | 87 | # NCrunch 88 | *.ncrunch* 89 | .*crunch*.local.xml 90 | 91 | # Installshield output folder 92 | [Ee]xpress/ 93 | 94 | # DocProject is a documentation generator add-in 95 | DocProject/buildhelp/ 96 | DocProject/Help/*.HxT 97 | DocProject/Help/*.HxC 98 | DocProject/Help/*.hhc 99 | DocProject/Help/*.hhk 100 | DocProject/Help/*.hhp 101 | DocProject/Help/Html2 102 | DocProject/Help/html 103 | 104 | # Click-Once directory 105 | publish/ 106 | 107 | # Publish Web Output 108 | *.Publish.xml 109 | 110 | # Windows Azure Build Output 111 | csx 112 | *.build.csdef 113 | 114 | # Windows Store app package directory 115 | AppPackages/ 116 | 117 | node_modules 118 | src/GitMonitor/wwwroot/lib/* 119 | !src/GitMonitor/wwwroot/lib/bootstrap 120 | 121 | # Others 122 | #sql/ 123 | *.Cache 124 | ClientBin/ 125 | [Ss]tyle[Cc]op.* 126 | ~$* 127 | *~ 128 | *.dbmdl 129 | *.[Pp]ublish.xml 130 | *.publishsettings 131 | 132 | # RIA/Silverlight projects 133 | Generated_Code/ 134 | 135 | # Backup & report files from converting an old project file to a newer 136 | # Visual Studio version. Backup files are not needed, because we have git ;-) 137 | _UpgradeReport_Files/ 138 | Backup*/ 139 | UpgradeLog*.XML 140 | UpgradeLog*.htm 141 | 142 | # SQL Server files 143 | #App_Data/*.mdf 144 | #App_Data/*.ldf 145 | 146 | 147 | #LightSwitch generated files 148 | GeneratedArtifacts/ 149 | _Pvt_Extensions/ 150 | ModelManifest.xml 151 | 152 | # ========================= 153 | # Windows detritus 154 | # ========================= 155 | 156 | # Windows image file caches 157 | Thumbs.db 158 | ehthumbs.db 159 | 160 | # Folder config file 161 | Desktop.ini 162 | 163 | # Recycle Bin used on file shares 164 | $RECYCLE.BIN/ 165 | 166 | # Mac desktop service store files 167 | .DS_Store 168 | 169 | 170 | # Scala Stuff 171 | target/ 172 | .idea/ 173 | 174 | #Python Stuff 175 | __pycache__ -------------------------------------------------------------------------------- /Export/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Export/ExcelHelper.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Export 5 | { 6 | using System; 7 | using System.IO; 8 | using Microsoft.Office.Interop.Excel; 9 | 10 | public class ExcelHelper 11 | { 12 | private static Workbook workbook; 13 | private static Worksheet worksheet; 14 | private static Application excelApp; 15 | 16 | public ExcelHelper(string fileName) 17 | { 18 | excelApp = new Application { DisplayAlerts = false }; 19 | 20 | if (string.IsNullOrEmpty(fileName)) 21 | { 22 | workbook = excelApp.Workbooks.Add(); 23 | worksheet = (Worksheet)workbook.Sheets.Add(); 24 | } 25 | else 26 | { 27 | var spreadsheetLocation = Path.Combine(Directory.GetCurrentDirectory(), fileName); 28 | workbook = excelApp.Workbooks.Open(spreadsheetLocation); 29 | } 30 | } 31 | 32 | public void AddWorksheet(string name) 33 | { 34 | worksheet = (Worksheet)workbook.Sheets.Add(); 35 | worksheet.Name = name; 36 | } 37 | 38 | public void GetWorksheet(string name) 39 | { 40 | worksheet = (Worksheet)workbook.Worksheets[name]; 41 | } 42 | 43 | public void WriteHeaderRow(string headings) 44 | { 45 | int c = 1; 46 | foreach (string header in headings.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) 47 | { 48 | worksheet.Cells[1, c] = header; 49 | c++; 50 | } 51 | } 52 | 53 | public void SaveWorkBook(string path, bool append) 54 | { 55 | if (append) 56 | { 57 | workbook.Save(); 58 | } 59 | else 60 | { 61 | workbook.SaveAs(path, XlFileFormat.xlWorkbookDefault, Type.Missing, Type.Missing, Type.Missing, Type.Missing, XlSaveAsAccessMode.xlNoChange, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing); 62 | } 63 | } 64 | 65 | public void Close() 66 | { 67 | workbook.Close(true); 68 | excelApp.Quit(); 69 | excelApp = null; 70 | } 71 | 72 | public void Write(int row, int column, dynamic content) 73 | { 74 | worksheet.Cells[row, column] = content; 75 | } 76 | 77 | public void Write(object[,] data, int rows, int columns) 78 | { 79 | var startCell = (Range)worksheet.Cells[2, 1]; 80 | var endCell = (Range)worksheet.Cells[rows + 1, columns]; 81 | var writeRange = worksheet.Range[startCell, endCell]; 82 | writeRange.Value2 = data; 83 | } 84 | 85 | public void Append(object[,] data, int rows, int columns) 86 | { 87 | int lastUsedRow = worksheet.Cells.SpecialCells(XlCellType.xlCellTypeLastCell, Type.Missing).Row; 88 | var startCell = (Range)worksheet.Cells[lastUsedRow + 1, 1]; 89 | var endCell = (Range)worksheet.Cells[lastUsedRow + rows, columns]; 90 | var writeRange = worksheet.Range[startCell, endCell]; 91 | writeRange.Value2 = data; 92 | } 93 | 94 | public int GetLastRow(string file) 95 | { 96 | return worksheet.Cells.Find( 97 | "*", 98 | worksheet.Cells[1, 1], 99 | XlFindLookIn.xlFormulas, 100 | XlLookAt.xlPart, 101 | XlSearchOrder.xlByRows, 102 | XlSearchDirection.xlPrevious, 103 | Type.Missing, 104 | Type.Missing, 105 | Type.Missing).Row + 1; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Export/Export.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Exe 4 | net472 5 | FreeToDev 6 | Mike Fourie 7 | Mike Fourie 8 | https://opensource.org/licenses/MIT 9 | Commandline Export Utility for Git Commits 10 | GitMonitor Export 11 | gme 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Export/Options.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Export 5 | { 6 | using System; 7 | using CommandLine; 8 | 9 | public class Options 10 | { 11 | [Option('s', "service-endpoint", HelpText = "The service endpoint to execute rest calls against", Required = true)] 12 | public string ServiceEndPoint { get; set; } 13 | 14 | [Option('d', "days", HelpText = "The number of days to retrieve", Required = false, DefaultValue = 10)] 15 | public int Days { get; set; } 16 | 17 | [Option('t', "startdate", HelpText = "The startdate", Required = false)] 18 | public DateTime StartDate { get; set; } 19 | 20 | [Option('e', "enddate", HelpText = "The enddate", Required = false)] 21 | public DateTime EndDate { get; set; } 22 | 23 | [Option('r', "repositoryname", HelpText = "The name of the repository to retrieve", Required = false)] 24 | public string RepositoryName { get; set; } 25 | 26 | [Option('b', "branchname", HelpText = "The name of the branch to retrieve", Required = false, DefaultValue = "master")] 27 | public string BranchName { get; set; } 28 | 29 | [Option('f', "filename", HelpText = "The name of the file to save results to", Required = false)] 30 | public string FileName { get; set; } 31 | 32 | [Option('m', "monitoredpathname", HelpText = "The name of the monitored path to query", Required = false, DefaultValue = "default")] 33 | public string MonitoredPathName { get; set; } 34 | 35 | [Option('a', "append", HelpText = "Set to true to append to the file provided", Required = false)] 36 | public bool Append { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Export/Program.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Export 5 | { 6 | using System; 7 | using System.Globalization; 8 | using System.IO; 9 | using System.Net.Http; 10 | using System.Net.Http.Headers; 11 | using System.Threading.Tasks; 12 | using GitMonitor.Models; 13 | using Newtonsoft.Json; 14 | 15 | public class Program 16 | { 17 | private static readonly Options Arguments = new Options(); 18 | private static ExcelHelper myExcel; 19 | 20 | public static void Main(string[] args) 21 | { 22 | Console.WriteLine("--------------------------------------------------"); 23 | Console.WriteLine("Git Monitor Exporter (c) Mike Fourie - FreeToDev"); 24 | Console.WriteLine("--------------------------------------------------"); 25 | 26 | if (args == null || args.Length == 0) 27 | { 28 | Console.WriteLine("Please provide valid arguments, e.g. --service-endpoint [YOUR URL] --monitoredpathname default --days 180"); 29 | return; 30 | } 31 | 32 | CommandLine.Parser.Default.ParseArguments(args, Arguments); 33 | if (Arguments.Append && string.IsNullOrEmpty(Arguments.FileName)) 34 | { 35 | Console.WriteLine("A filename must be provided when append is true."); 36 | return; 37 | } 38 | 39 | if (!Arguments.Append) 40 | { 41 | myExcel = new ExcelHelper(string.Empty); 42 | myExcel.AddWorksheet("CommitData"); 43 | myExcel.WriteHeaderRow("Sha,CommitUrl,Author,AuthorEmail,AuthorWhen,Committer,CommitterEmail,CommitterWhen,CommitterWhenShort,IsMerge,Message,RepositoryFriendlyName,RepositoryName,BranchName,DayOfWeek,WeekOfYear,Month,Year"); 44 | } 45 | else 46 | { 47 | myExcel = new ExcelHelper(Arguments.FileName); 48 | } 49 | 50 | Console.WriteLine($"{DateTime.Now.ToLongTimeString()} --- Export Started for {Arguments.BranchName}"); 51 | RunAsync().Wait(); 52 | Console.WriteLine($"{DateTime.Now.ToLongTimeString()} --- Export Completed"); 53 | } 54 | 55 | private static async Task RunAsync() 56 | { 57 | using (HttpClient client = new HttpClient()) 58 | { 59 | client.BaseAddress = new Uri(Arguments.ServiceEndPoint); 60 | client.DefaultRequestHeaders.Accept.Clear(); 61 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 62 | 63 | string urltopass; 64 | if (Arguments.StartDate != DateTime.MinValue) 65 | { 66 | if (Arguments.EndDate != DateTime.MinValue) 67 | { 68 | urltopass = string.IsNullOrEmpty(Arguments.RepositoryName) ? $"/api/commits/{Arguments.MonitoredPathName}/{Arguments.StartDate:dd MMM yyyy}/{Arguments.EndDate:dd MMM yyyy}?branchName={Arguments.BranchName}" : $"/api/commits/{Arguments.MonitoredPathName}/{Arguments.StartDate:dd MMM yyyy}/{Arguments.EndDate:dd MMM yyyy}?repoName={Arguments.RepositoryName}&branchName={Arguments.BranchName}"; 69 | } 70 | else 71 | { 72 | urltopass = string.IsNullOrEmpty(Arguments.RepositoryName) ? $"/api/commits/{Arguments.MonitoredPathName}/{Arguments.StartDate:dd MMM yyyy}?branchName={Arguments.BranchName}" : $"/api/commits/{Arguments.MonitoredPathName}/{Arguments.StartDate:dd MMM yyyy}?branchName={Arguments.BranchName}?repoName={Arguments.RepositoryName}&branchName={Arguments.BranchName}"; 73 | } 74 | } 75 | else 76 | { 77 | urltopass = string.IsNullOrEmpty(Arguments.RepositoryName) ? $"/api/commits/{Arguments.MonitoredPathName}?days={Arguments.Days}&branchName={Arguments.BranchName}" : $"/api/commits/{Arguments.MonitoredPathName}?repoName={Arguments.RepositoryName}&branchName={Arguments.BranchName}&days={Arguments.Days}"; 78 | } 79 | 80 | var response = client.GetAsync(urltopass).Result; 81 | string content = await response.Content.ReadAsStringAsync(); 82 | if (response.IsSuccessStatusCode) 83 | { 84 | var mi = JsonConvert.DeserializeObject(content); 85 | Console.WriteLine($"{DateTime.Now.ToLongTimeString()} --- Processing {mi.CommitCount} Results"); 86 | if (!Arguments.Append) 87 | { 88 | myExcel.WriteHeaderRow("Sha,CommitUrl,Author,AuthorEmail,AuthorWhen,Committer,CommitterEmail,CommitterWhen,CommitterWhenShort,IsMerge,Message,RepositoryFriendlyName,RepositoryName,BranchName,DayOfWeek,WeekOfYear,Month,Year"); 89 | } 90 | else 91 | { 92 | myExcel.GetWorksheet("CommitData"); 93 | } 94 | 95 | var culture = new CultureInfo("en-US"); 96 | var calendar = culture.Calendar; 97 | int row = 0; 98 | int colcount = 18; 99 | var data = new object[mi.CommitCount, colcount]; 100 | foreach (var commit in mi.Commits) 101 | { 102 | int column = 0; 103 | data[row, column++] = commit.Sha; 104 | data[row, column++] = commit.CommitUrl; 105 | data[row, column++] = commit.Author; 106 | data[row, column++] = commit.AuthorEmail; 107 | data[row, column++] = commit.AuthorWhen.ToString("F"); 108 | data[row, column++] = commit.Committer; 109 | data[row, column++] = commit.CommitterEmail; 110 | data[row, column++] = commit.CommitterWhen.ToString("F"); 111 | data[row, column++] = commit.CommitterWhen.ToString("dd MMM yy"); 112 | data[row, column++] = commit.IsMerge; 113 | data[row, column++] = commit.Message; 114 | data[row, column++] = commit.RepositoryFriendlyName; 115 | data[row, column++] = commit.RepositoryName; 116 | data[row, column++] = Arguments.BranchName; 117 | data[row, column++] = commit.CommitterWhen.ToString("ddd"); 118 | data[row, column++] = calendar.GetWeekOfYear(commit.CommitterWhen, culture.DateTimeFormat.CalendarWeekRule, culture.DateTimeFormat.FirstDayOfWeek); 119 | data[row, column++] = commit.CommitterWhen.ToString("MMMM"); 120 | data[row, column] = commit.CommitterWhen.ToString("yyyy"); 121 | row++; 122 | } 123 | 124 | Console.WriteLine($"{DateTime.Now.ToLongTimeString()} --- Writing Data"); 125 | if (Arguments.Append) 126 | { 127 | myExcel.Append(data, mi.CommitCount, colcount); 128 | } 129 | else 130 | { 131 | myExcel.Write(data, mi.CommitCount, colcount); 132 | } 133 | 134 | string fileName = Arguments.FileName; 135 | if (string.IsNullOrWhiteSpace(fileName)) 136 | { 137 | fileName = DateTime.Now.ToString("dd MMM yy hh-mm") + " ChangesetData.xlsx"; 138 | } 139 | 140 | myExcel.SaveWorkBook(Path.Combine(Environment.CurrentDirectory, fileName), Arguments.Append); 141 | myExcel.Close(); 142 | } 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Export/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyCulture("")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("b22f713e-c293-41cc-ad95-280f178d1f63")] 19 | 20 | // Version information for an assembly consists of the following four values: 21 | // 22 | // Major Version 23 | // Minor Version 24 | // Build Number 25 | // Revision 26 | // 27 | // You can specify all the values or you can default the Build and Revision Numbers 28 | // by using the '*' as shown below: 29 | // [assembly: AssemblyVersion("1.0.*")] 30 | // [assembly: AssemblyVersion("1.0.0.0")] 31 | // [assembly: AssemblyFileVersion("1.0.0.0")] 32 | -------------------------------------------------------------------------------- /GitMonitor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.16 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitMonitor", "src\GitMonitor\GitMonitor.csproj", "{6C624E59-004D-47A3-90B5-05A5AB4D912D}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Export", "Export\Export.csproj", "{B22F713E-C293-41CC-AD95-280F178D1F63}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {6C624E59-004D-47A3-90B5-05A5AB4D912D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {6C624E59-004D-47A3-90B5-05A5AB4D912D}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {6C624E59-004D-47A3-90B5-05A5AB4D912D}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {6C624E59-004D-47A3-90B5-05A5AB4D912D}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B22F713E-C293-41CC-AD95-280F178D1F63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B22F713E-C293-41CC-AD95-280F178D1F63}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B22F713E-C293-41CC-AD95-280F178D1F63}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B22F713E-C293-41CC-AD95-280F178D1F63}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {D903F11E-DB07-4940-A05D-DAC66F7412BC} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Mike Fourie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitMonitor 2 | Monitor and query changes across a collection of Git repositories 3 | 4 | The introductory blog post can be found here - https://mikefourie.wordpress.com/2016/04/24/gitmonitor/ 5 | 6 | 7 | # Web Api 8 | **Query commits by previous days** 9 | ---- 10 | Returns commits from now over the previous specified days. 11 | 12 | * **URL** 13 | /api/commits/[days] 14 | 15 | * **Required Params** 16 | 17 | `days=[int]` 18 | 19 | * **Sample Calls** 20 | * 10 days commits in default monitoredPathName: ```/api/commits/10``` 21 | 22 | **Query commits by date range** 23 | ---- 24 | Returns commits between the provided dates (inclusive). 25 | 26 | * **URL** 27 | /api/commits/[startdate]/[enddate] 28 | 29 | * **Required Params** 30 | 31 | `startdate=[datetime]` 32 | 33 | * **Sample Calls** 34 | * Commits between 1 Jan 2015 and today: ```/api/commits/1 Jan 2015``` 35 | * Commits between 1 Jan 2016 and 30 Jan 2016 today: ```/api/commits/1 Jan 2016/30 Jan 2016``` 36 | 37 | **Find a commit** 38 | ---- 39 | Returns commits between the provided dates (inclusive). 40 | 41 | * **URL** 42 | /api/search/[monitoredPathName]/[commit] 43 | 44 | * **Required Params** 45 | 46 | `commit=[sha1]` 47 | 48 | * **Sample Calls** 49 | * Find commit in default monitoredPathName: ```/api/search/a3er4w``` 50 | * Find commit in work monitoredPathName: ```/api/search/work/a3er4w``` 51 | * Find commit in all paths: ```/api/search/*/a3er4w``` 52 | -------------------------------------------------------------------------------- /Settings.StyleCop: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | False 8 | 9 | 10 | 11 | 12 | False 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.9.0.1/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | # open json.net splash page on package install 4 | # don't open if json.net is installed as a dependency 5 | 6 | try 7 | { 8 | $url = "http://www.newtonsoft.com/json/install?version=" + $package.Version 9 | $dte2 = Get-Interface $dte ([EnvDTE80.DTE2]) 10 | 11 | if ($dte2.ActiveWindow.Caption -eq "Package Manager Console") 12 | { 13 | # user is installing from VS NuGet console 14 | # get reference to the window, the console host and the input history 15 | # show webpage if "install-package newtonsoft.json" was last input 16 | 17 | $consoleWindow = $(Get-VSComponentModel).GetService([NuGetConsole.IPowerConsoleWindow]) 18 | 19 | $props = $consoleWindow.GetType().GetProperties([System.Reflection.BindingFlags]::Instance -bor ` 20 | [System.Reflection.BindingFlags]::NonPublic) 21 | 22 | $prop = $props | ? { $_.Name -eq "ActiveHostInfo" } | select -first 1 23 | if ($prop -eq $null) { return } 24 | 25 | $hostInfo = $prop.GetValue($consoleWindow) 26 | if ($hostInfo -eq $null) { return } 27 | 28 | $history = $hostInfo.WpfConsole.InputHistory.History 29 | 30 | $lastCommand = $history | select -last 1 31 | 32 | if ($lastCommand) 33 | { 34 | $lastCommand = $lastCommand.Trim().ToLower() 35 | if ($lastCommand.StartsWith("install-package") -and $lastCommand.Contains("newtonsoft.json")) 36 | { 37 | $dte2.ItemOperations.Navigate($url) | Out-Null 38 | } 39 | } 40 | } 41 | else 42 | { 43 | # user is installing from VS NuGet dialog 44 | # get reference to the window, then smart output console provider 45 | # show webpage if messages in buffered console contains "installing...newtonsoft.json" in last operation 46 | 47 | $instanceField = [NuGet.Dialog.PackageManagerWindow].GetField("CurrentInstance", [System.Reflection.BindingFlags]::Static -bor ` 48 | [System.Reflection.BindingFlags]::NonPublic) 49 | 50 | $consoleField = [NuGet.Dialog.PackageManagerWindow].GetField("_smartOutputConsoleProvider", [System.Reflection.BindingFlags]::Instance -bor ` 51 | [System.Reflection.BindingFlags]::NonPublic) 52 | 53 | if ($instanceField -eq $null -or $consoleField -eq $null) { return } 54 | 55 | $instance = $instanceField.GetValue($null) 56 | 57 | if ($instance -eq $null) { return } 58 | 59 | $consoleProvider = $consoleField.GetValue($instance) 60 | if ($consoleProvider -eq $null) { return } 61 | 62 | $console = $consoleProvider.CreateOutputConsole($false) 63 | 64 | $messagesField = $console.GetType().GetField("_messages", [System.Reflection.BindingFlags]::Instance -bor ` 65 | [System.Reflection.BindingFlags]::NonPublic) 66 | if ($messagesField -eq $null) { return } 67 | 68 | $messages = $messagesField.GetValue($console) 69 | if ($messages -eq $null) { return } 70 | 71 | $operations = $messages -split "==============================" 72 | 73 | $lastOperation = $operations | select -last 1 74 | 75 | if ($lastOperation) 76 | { 77 | $lastOperation = $lastOperation.ToLower() 78 | 79 | $lines = $lastOperation -split "`r`n" 80 | 81 | $installMatch = $lines | ? { $_.StartsWith("------- installing...newtonsoft.json ") } | select -first 1 82 | 83 | if ($installMatch) 84 | { 85 | $dte2.ItemOperations.Navigate($url) | Out-Null 86 | } 87 | } 88 | } 89 | } 90 | catch 91 | { 92 | try 93 | { 94 | $pmPane = $dte2.ToolWindows.OutputWindow.OutputWindowPanes.Item("Package Manager") 95 | 96 | $selection = $pmPane.TextDocument.Selection 97 | $selection.StartOfDocument($false) 98 | $selection.EndOfDocument($true) 99 | 100 | if ($selection.Text.StartsWith("Attempting to gather dependencies information for package 'Newtonsoft.Json." + $package.Version + "'")) 101 | { 102 | # don't show on upgrade 103 | if (!$selection.Text.Contains("Removed package")) 104 | { 105 | $dte2.ItemOperations.Navigate($url) | Out-Null 106 | } 107 | } 108 | } 109 | catch 110 | { 111 | # stop potential errors from bubbling up 112 | # worst case the splash page won't open 113 | } 114 | } 115 | 116 | # still yolo -------------------------------------------------------------------------------- /src/GitMonitor/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "wwwroot/lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/GitMonitor/Controllers/Api/GitBranchSearchController.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // Copyright Mike Fourie 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Controllers 5 | { 6 | using GitMonitor.Models; 7 | using GitMonitor.Repositories; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | 12 | [Route("api/branches")] 13 | public class GitBranchSearchController : Controller 14 | { 15 | private readonly ILogger locallogger; 16 | private readonly ICommitRepository localRepository; 17 | private readonly IOptions localMonitoredPathConfig; 18 | 19 | public GitBranchSearchController(ICommitRepository repository, ILogger logger, IOptions monitoredPathConfig) 20 | { 21 | this.localRepository = repository; 22 | this.locallogger = logger; 23 | this.localMonitoredPathConfig = monitoredPathConfig; 24 | } 25 | 26 | [Route("{repositoryName}/{sha}")] 27 | public JsonResult GetBranches(string repositoryName, string sha) 28 | { 29 | GitSearch search = new GitSearch 30 | { 31 | Sha = sha, 32 | Branches = this.localRepository.SearchBranchesForCommit(this.localMonitoredPathConfig.Value, repositoryName, sha, string.Empty) 33 | }; 34 | 35 | return this.Json(search); 36 | } 37 | 38 | [Route("{repositoryName}/{sha}/{filter}")] 39 | public JsonResult GetBranches(string repositoryName, string sha, string filter) 40 | { 41 | GitSearch search = new GitSearch 42 | { 43 | Sha = sha, 44 | Branches = this.localRepository.SearchBranchesForCommit(this.localMonitoredPathConfig.Value, repositoryName, sha, filter) 45 | }; 46 | return this.Json(search); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/GitMonitor/Controllers/Api/GitCommitController.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // Copyright Mike Fourie 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Controllers 5 | { 6 | using System; 7 | using GitMonitor.Models; 8 | using GitMonitor.Repositories; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Options; 12 | 13 | [Route("api/commits")] 14 | public class GitCommitController : Controller 15 | { 16 | private readonly ILogger locallogger; 17 | private readonly ICommitRepository localRepository; 18 | private readonly IOptions localMonitoredPathConfig; 19 | 20 | public GitCommitController(ICommitRepository repository, ILogger logger, IOptions monitoredPathConfig) 21 | { 22 | this.localRepository = repository; 23 | this.locallogger = logger; 24 | this.localMonitoredPathConfig = monitoredPathConfig; 25 | } 26 | 27 | [Route("{days:int?}")] 28 | public JsonResult Get(int days) 29 | { 30 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, string.Empty, string.Empty, string.Empty, days); 31 | return this.Json(results); 32 | } 33 | 34 | [Route("{monitoredPathName}/{days:int?}")] 35 | public JsonResult Get(string monitoredPathName, int days) 36 | { 37 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, string.Empty, string.Empty, days); 38 | return this.Json(results); 39 | } 40 | 41 | [Route("{monitoredPathName}/{repoName}/{days:int?}")] 42 | public JsonResult Get(string monitoredPathName, string repoName, int days) 43 | { 44 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, repoName, string.Empty, days); 45 | return this.Json(results); 46 | } 47 | 48 | [Route("{monitoredPathName}")] 49 | public JsonResult Get(string monitoredPathName, string repoName, string branchName, int days) 50 | { 51 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, repoName, branchName, days); 52 | return this.Json(results); 53 | } 54 | 55 | [Route("{startDateTime:DateTime}")] 56 | public JsonResult Get(DateTime startDateTime, string repoName, string branchName) 57 | { 58 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, string.Empty, repoName, branchName, startDateTime, DateTime.UtcNow); 59 | return this.Json(results); 60 | } 61 | 62 | [Route("{monitoredPathName}/{startDateTime:DateTime}")] 63 | public JsonResult Get(string monitoredPathName, string repoName, string branchName, DateTime startDateTime) 64 | { 65 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, repoName, branchName, startDateTime, DateTime.UtcNow); 66 | return this.Json(results); 67 | } 68 | 69 | [Route("{startDateTime:DateTime}/{endDateTime:DateTime}")] 70 | public JsonResult Get(DateTime startDateTime, DateTime endDateTime, string repoName, string branchName) 71 | { 72 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, string.Empty, repoName, branchName, startDateTime, endDateTime); 73 | return this.Json(results); 74 | } 75 | 76 | [Route("{monitoredPathName}/{startDateTime:DateTime}/{endDateTime:DateTime}")] 77 | public JsonResult Get(string monitoredPathName, DateTime startDateTime, DateTime endDateTime, string repoName, string branchName) 78 | { 79 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, repoName, branchName, startDateTime, endDateTime); 80 | return this.Json(results); 81 | } 82 | 83 | [Route("{monitoredPathName}/{commitId}")] 84 | public JsonResult Get(string monitoredPathName, string commitId, string repoName, string branchName) 85 | { 86 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, repoName, branchName, commitId); 87 | return this.Json(results); 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /src/GitMonitor/Controllers/Api/GitRepositoryController.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // Copyright Mike Fourie 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Controllers 5 | { 6 | using System; 7 | using GitMonitor.Models; 8 | using GitMonitor.Repositories; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Options; 12 | 13 | [Route("api/repositories")] 14 | public class GitRepositoryController : Controller 15 | { 16 | private readonly ILogger locallogger; 17 | private readonly IRepoRepository localRepoRepository; 18 | private readonly IOptions localMonitoredPathConfig; 19 | 20 | public GitRepositoryController(IRepoRepository repository, ILogger logger, IOptions monitoredPathConfig) 21 | { 22 | this.localRepoRepository = repository; 23 | this.locallogger = logger; 24 | this.localMonitoredPathConfig = monitoredPathConfig; 25 | } 26 | 27 | [Route("{monitoredPathName}")] 28 | public JsonResult Get(string monitoredPathName) 29 | { 30 | var results = this.localRepoRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, string.Empty); 31 | return this.Json(results); 32 | } 33 | 34 | [Route("{monitoredPathName}/{repoName}")] 35 | public JsonResult Get(string monitoredPathName, string repoName) 36 | { 37 | var results = this.localRepoRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, repoName); 38 | return this.Json(results); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/GitMonitor/Controllers/Api/GitSearchController.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // Copyright Mike Fourie 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Controllers 5 | { 6 | using GitMonitor.Models; 7 | using GitMonitor.Repositories; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | 12 | [Route("api/search")] 13 | public class GitSearchController : Controller 14 | { 15 | private readonly ILogger locallogger; 16 | private readonly ICommitRepository localRepository; 17 | private readonly IOptions localMonitoredPathConfig; 18 | 19 | public GitSearchController(ICommitRepository repository, ILogger logger, IOptions monitoredPathConfig) 20 | { 21 | this.localRepository = repository; 22 | this.locallogger = logger; 23 | this.localMonitoredPathConfig = monitoredPathConfig; 24 | } 25 | 26 | [Route("{sha}")] 27 | public JsonResult Get(string sha) 28 | { 29 | GitSearch search = new GitSearch 30 | { 31 | Sha = sha, 32 | Commits = this.localRepository.SearchForCommit(this.localMonitoredPathConfig.Value, string.Empty, sha) 33 | }; 34 | return this.Json(search); 35 | } 36 | 37 | [Route("{monitoredPathName}/{sha}")] 38 | public JsonResult Get(string monitoredPathName, string sha) 39 | { 40 | GitSearch search = new GitSearch 41 | { 42 | Sha = sha, 43 | Commits = this.localRepository.SearchForCommit(this.localMonitoredPathConfig.Value, monitoredPathName, sha) 44 | }; 45 | return this.Json(search); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/GitMonitor/Controllers/Web/HomeController.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Controllers 5 | { 6 | using System; 7 | using GitMonitor.Models; 8 | using GitMonitor.Repositories; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Options; 12 | 13 | public class HomeController : Controller 14 | { 15 | private readonly ILogger locallogger; 16 | private readonly ICommitRepository localRepository; 17 | private readonly IOptions localMonitoredPathConfig; 18 | 19 | public HomeController(ICommitRepository repository, ILogger logger, IOptions monitoredPathConfig) 20 | { 21 | this.localRepository = repository; 22 | this.locallogger = logger; 23 | this.localMonitoredPathConfig = monitoredPathConfig; 24 | } 25 | 26 | public IActionResult Index(string monitoredPathName, string branchName, int days, DateTime startDateTime, DateTime endDateTime, string sha) 27 | { 28 | this.ViewData["MPName"] = monitoredPathName; 29 | this.localMonitoredPathConfig.Value.Search = null; 30 | if (!string.IsNullOrEmpty(sha)) 31 | { 32 | GitSearch search = new GitSearch(); 33 | if (!string.IsNullOrEmpty(sha)) 34 | { 35 | search.Sha = sha; 36 | search.Commits = this.localRepository.SearchForCommit(this.localMonitoredPathConfig.Value, "*", sha); 37 | } 38 | 39 | MonitoredPathConfig mpc = this.localMonitoredPathConfig.Value; 40 | mpc.Search = search; 41 | return this.View(mpc); 42 | } 43 | 44 | if (startDateTime == DateTime.MinValue) 45 | { 46 | this.ViewData["MPDays"] = days; 47 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, branchName, days); 48 | return this.View(results); 49 | } 50 | else 51 | { 52 | this.ViewData["StartDateTime"] = startDateTime.ToString("yyyy-MM-dd"); 53 | this.ViewData["EndDateTime"] = endDateTime.ToString("yyyy-MM-dd"); 54 | 55 | var results = this.localRepository.Get(this.localMonitoredPathConfig.Value, monitoredPathName, branchName, startDateTime, endDateTime); 56 | return this.View(results); 57 | } 58 | } 59 | 60 | public JsonResult GetCommmit(string sha) 61 | { 62 | GitSearch search = new GitSearch(); 63 | if (!string.IsNullOrEmpty(sha)) 64 | { 65 | search.Sha = sha; 66 | search.Commits = this.localRepository.SearchForCommit(this.localMonitoredPathConfig.Value, "*", sha); 67 | } 68 | 69 | // MonitoredPathConfig mpc = this.localMonitoredPathConfig.Value; 70 | // mpc.Search = search; 71 | return this.Json(search); 72 | } 73 | 74 | public IActionResult Fetch(string name, int days) 75 | { 76 | this.localRepository.FetchAll(this.localMonitoredPathConfig.Value, name); 77 | return this.RedirectToAction("Index", new { name, days }); 78 | } 79 | 80 | public IActionResult Error() 81 | { 82 | return this.View(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/GitMonitor/GitMonitor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.2 4 | false 5 | 3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/GitMonitor/Models/GitCommit.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Models 5 | { 6 | using System; 7 | 8 | public class GitCommit 9 | { 10 | public string CommitUrl { get; set; } 11 | 12 | public string Author { get; set; } 13 | 14 | public string AuthorEmail { get; set; } 15 | 16 | public DateTime AuthorWhen { get; set; } 17 | 18 | public string Committer { get; set; } 19 | 20 | public string CommitterEmail { get; set; } 21 | 22 | public DateTime CommitterWhen { get; set; } 23 | 24 | public string Message { get; set; } 25 | 26 | public string Sha { get; set; } 27 | 28 | public bool IsMerge { get; set; } 29 | 30 | public string RepositoryFriendlyName { get; set; } 31 | 32 | public string RepositoryName { get; set; } 33 | 34 | public string RepositoryUrl { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/GitMonitor/Models/GitRepository.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Models 5 | { 6 | public class GitRepository 7 | { 8 | public string Name { get; set; } 9 | 10 | public string FriendlyName { get; set; } 11 | 12 | public string CommitUrl { get; set; } 13 | 14 | public bool AllowFetch { get; set; } 15 | 16 | public int CommitCount { get; set; } 17 | 18 | public GitCommit LastCommit { get; set; } 19 | 20 | public int BranchCount { get; set; } 21 | 22 | public string Branch { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/GitMonitor/Models/GitSearch.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Models 5 | { 6 | using System.Collections.Generic; 7 | 8 | public class GitSearch 9 | { 10 | public string Sha { get; set; } 11 | 12 | public List Commits { get; set; } 13 | 14 | public List Branches { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/GitMonitor/Models/MonitoredPath.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Models 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | public class MonitoredPath 10 | { 11 | public MonitoredPath() 12 | { 13 | this.Commits = new List(); 14 | this.Repositories = new List(); 15 | } 16 | 17 | public string Name { get; set; } 18 | 19 | public string Path { get; set; } 20 | 21 | public bool AllFolders { get; set; } 22 | 23 | public bool AllowFetch { get; set; } 24 | 25 | public bool IncludeMergeCommits { get; set; } 26 | 27 | public int Days { get; set; } 28 | 29 | public DateTime StartDateTime { get; set; } 30 | 31 | public DateTime EndDateTime { get; set; } 32 | 33 | public int CommitCount { get; set; } 34 | 35 | public List Repositories { get; set; } 36 | 37 | public List Commits { get; set; } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/GitMonitor/Models/MonitoredPathConfig.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Models 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | public class MonitoredPathConfig 10 | { 11 | public MonitoredPathConfig() 12 | { 13 | this.MonitoredPaths = new List(); 14 | } 15 | 16 | public List MonitoredPaths { get; set; } 17 | 18 | public MonitoredPath ActiveMonitoredPath { get; set; } 19 | 20 | public DateTime StartDateTime { get; set; } 21 | 22 | public DateTime EndDateTime { get; set; } 23 | 24 | public GitSearch Search { get; set; } 25 | 26 | public string DefaultDays { get; set; } 27 | 28 | public string DefaultRemote { get; set; } 29 | 30 | public string DefaultBranch { get; set; } 31 | 32 | public string DefaultUserNameExcludeFilter { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/GitMonitor/Program.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor 5 | { 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | BuildWebHost(args).Run(); 14 | } 15 | 16 | public static IWebHost BuildWebHost(string[] args) => 17 | WebHost.CreateDefaultBuilder(args) 18 | .UseStartup() 19 | .Build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/GitMonitor/Properties/PublishProfiles/FileSystem-publish.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 2 | # Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | [cmdletbinding(SupportsShouldProcess=$true)] 5 | param($publishProperties=@{}, $packOutput, $pubProfilePath, $nugetUrl) 6 | 7 | # to learn more about this file visit https://go.microsoft.com/fwlink/?LinkId=524327 8 | $publishModuleVersion = '1.1.0' 9 | 10 | function Get-PublishModulePath{ 11 | [cmdletbinding()] 12 | param() 13 | process{ 14 | $keysToCheck = @('hklm:\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\{0}', 15 | 'hklm:\SOFTWARE\Microsoft\VisualStudio\{0}', 16 | 'hklm:\SOFTWARE\Wow6432Node\Microsoft\VWDExpress\{0}', 17 | 'hklm:\SOFTWARE\Microsoft\VWDExpress\{0}' 18 | ) 19 | $versions = @('14.0', '15.0') 20 | 21 | [string]$publishModulePath=$null 22 | :outer foreach($keyToCheck in $keysToCheck){ 23 | foreach($version in $versions){ 24 | if(Test-Path ($keyToCheck -f $version) ){ 25 | $vsInstallPath = (Get-itemproperty ($keyToCheck -f $version) -Name InstallDir -ErrorAction SilentlyContinue | select -ExpandProperty InstallDir -ErrorAction SilentlyContinue) 26 | 27 | if($vsInstallPath){ 28 | $installedPublishModulePath = "{0}Extensions\Microsoft\Web Tools\Publish\Scripts\{1}\" -f $vsInstallPath, $publishModuleVersion 29 | if(!(Test-Path $installedPublishModulePath)){ 30 | $vsInstallPath = $vsInstallPath + 'VWDExpress' 31 | $installedPublishModulePath = "{0}Extensions\Microsoft\Web Tools\Publish\Scripts\{1}\" -f $vsInstallPath, $publishModuleVersion 32 | } 33 | if(Test-Path $installedPublishModulePath){ 34 | $publishModulePath = $installedPublishModulePath 35 | break outer; 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | $publishModulePath 43 | } 44 | } 45 | 46 | $publishModulePath = Get-PublishModulePath 47 | 48 | $defaultPublishSettings = New-Object psobject -Property @{ 49 | LocalInstallDir = $publishModulePath 50 | } 51 | 52 | function Enable-PackageDownloader{ 53 | [cmdletbinding()] 54 | param( 55 | $toolsDir = "$env:LOCALAPPDATA\Microsoft\Web Tools\Publish\package-downloader-$publishModuleVersion\", 56 | $pkgDownloaderDownloadUrl = 'https://go.microsoft.com/fwlink/?LinkId=524325') # package-downloader.psm1 57 | process{ 58 | if(get-module package-downloader){ 59 | remove-module package-downloader | Out-Null 60 | } 61 | 62 | if(!(get-module package-downloader)){ 63 | if(!(Test-Path $toolsDir)){ New-Item -Path $toolsDir -ItemType Directory -WhatIf:$false } 64 | 65 | $expectedPath = (Join-Path ($toolsDir) 'package-downloader.psm1') 66 | if(!(Test-Path $expectedPath)){ 67 | 'Downloading [{0}] to [{1}]' -f $pkgDownloaderDownloadUrl,$expectedPath | Write-Verbose 68 | (New-Object System.Net.WebClient).DownloadFile($pkgDownloaderDownloadUrl, $expectedPath) 69 | } 70 | 71 | if(!$expectedPath){throw ('Unable to download package-downloader.psm1')} 72 | 73 | 'importing module [{0}]' -f $expectedPath | Write-Output 74 | Import-Module $expectedPath -DisableNameChecking -Force 75 | } 76 | } 77 | } 78 | 79 | function Enable-PublishModule{ 80 | [cmdletbinding()] 81 | param() 82 | process{ 83 | if(get-module publish-module){ 84 | remove-module publish-module | Out-Null 85 | } 86 | 87 | if(!(get-module publish-module)){ 88 | $localpublishmodulepath = Join-Path $defaultPublishSettings.LocalInstallDir 'publish-module.psm1' 89 | if(Test-Path $localpublishmodulepath){ 90 | 'importing module [publish-module="{0}"] from local install dir' -f $localpublishmodulepath | Write-Verbose 91 | Import-Module $localpublishmodulepath -DisableNameChecking -Force 92 | $true 93 | } 94 | } 95 | } 96 | } 97 | 98 | try{ 99 | 100 | if (!(Enable-PublishModule)){ 101 | Enable-PackageDownloader 102 | Enable-NuGetModule -name 'publish-module' -version $publishModuleVersion -nugetUrl $nugetUrl 103 | } 104 | 105 | 'Calling Publish-AspNet' | Write-Verbose 106 | # call Publish-AspNet to perform the publish operation 107 | Publish-AspNet -publishProperties $publishProperties -packOutput $packOutput -pubProfilePath $pubProfilePath 108 | } 109 | catch{ 110 | "An error occurred during publish.`n{0}" -f $_.Exception.Message | Write-Error 111 | } -------------------------------------------------------------------------------- /src/GitMonitor/Properties/PublishProfiles/FileSystem.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | FileSystem 9 | Release 10 | Any CPU 11 | 12 | True 13 | False 14 | net452 15 | True 16 | .\bin\Release\PublishOutput 17 | True 18 | 19 | -------------------------------------------------------------------------------- /src/GitMonitor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17393/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "web": { 19 | "commandName": "web", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/GitMonitor/Repositories/CommitRepository.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Repositories 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using GitMonitor.Models; 11 | using LibGit2Sharp; 12 | using Microsoft.Extensions.Logging; 13 | 14 | public class CommitRepository : ICommitRepository 15 | { 16 | private readonly ILogger locallogger; 17 | 18 | public CommitRepository(ILogger logger) 19 | { 20 | this.locallogger = logger; 21 | } 22 | 23 | public MonitoredPathConfig Get(MonitoredPathConfig mpc, string monitoredPathName, string branchName, int days) 24 | { 25 | try 26 | { 27 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 28 | { 29 | monitoredPathName = "default"; 30 | } 31 | 32 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 33 | { 34 | if (string.Compare(mp.Name, monitoredPathName, StringComparison.OrdinalIgnoreCase) == 0) 35 | { 36 | mpc.ActiveMonitoredPath = this.GetMonitoredPath(mpc, mp, string.Empty, branchName, days); 37 | } 38 | } 39 | 40 | return mpc; 41 | } 42 | catch (Exception ex) 43 | { 44 | this.locallogger.LogError("Bad - ", ex); 45 | } 46 | 47 | return null; 48 | } 49 | 50 | public MonitoredPathConfig Get(MonitoredPathConfig mpc, string monitoredPathName, string branchName, DateTime startDateTime, DateTime endDateTime) 51 | { 52 | try 53 | { 54 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 55 | { 56 | monitoredPathName = "default"; 57 | } 58 | 59 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 60 | { 61 | if (string.Compare(mp.Name, monitoredPathName, StringComparison.OrdinalIgnoreCase) == 0) 62 | { 63 | mpc.ActiveMonitoredPath = this.GetMonitoredPath(mpc, mp, string.Empty, branchName, startDateTime, endDateTime); 64 | } 65 | } 66 | 67 | mpc.StartDateTime = startDateTime; 68 | mpc.EndDateTime = endDateTime; 69 | return mpc; 70 | } 71 | catch (Exception ex) 72 | { 73 | this.locallogger.LogError("Bad - ", ex); 74 | } 75 | 76 | return null; 77 | } 78 | 79 | public MonitoredPath Get(MonitoredPathConfig mpc, string monitoredPathName, string repoName, string branchName, int days) 80 | { 81 | MonitoredPath newmonitoredPath = new MonitoredPath(); 82 | try 83 | { 84 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 85 | { 86 | monitoredPathName = "default"; 87 | } 88 | 89 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 90 | { 91 | if (mp.Name.ToLower() == monitoredPathName.ToLower()) 92 | { 93 | newmonitoredPath = this.GetMonitoredPath(mpc, mp, repoName, branchName, days); 94 | } 95 | } 96 | } 97 | catch (Exception ex) 98 | { 99 | this.locallogger.LogError("Bad - ", ex); 100 | return null; 101 | } 102 | 103 | return newmonitoredPath; 104 | } 105 | 106 | public MonitoredPath Get(MonitoredPathConfig mpc, string monitoredPathName, string repoName, string branchName, DateTime startDateTime, DateTime endDateTime) 107 | { 108 | MonitoredPath newmonitoredPath = new MonitoredPath(); 109 | try 110 | { 111 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 112 | { 113 | monitoredPathName = "default"; 114 | } 115 | 116 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 117 | { 118 | if (mp.Name.ToLower() == monitoredPathName.ToLower()) 119 | { 120 | newmonitoredPath = this.GetMonitoredPath(mpc, mp, repoName, branchName, startDateTime, endDateTime); 121 | } 122 | } 123 | } 124 | catch (Exception ex) 125 | { 126 | this.locallogger.LogError("Bad - ", ex); 127 | return null; 128 | } 129 | 130 | return newmonitoredPath; 131 | } 132 | 133 | public List SearchBranchesForCommit(MonitoredPathConfig monitoredPathConfig, string repositoryName, string sha, string filter) 134 | { 135 | List branches = new List(); 136 | bool found = false; 137 | try 138 | { 139 | foreach (MonitoredPath mp in monitoredPathConfig.MonitoredPaths) 140 | { 141 | DirectoryInfo[] directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 142 | foreach (DirectoryInfo dir in directoriesToScan) 143 | { 144 | if (string.Compare(dir.Name, repositoryName, StringComparison.OrdinalIgnoreCase) == 0) 145 | { 146 | Repository r = new Repository($"{mp.Path}\\{repositoryName}"); 147 | IEnumerable b = this.ListBranchesContaininingCommit(r, sha, filter); 148 | 149 | foreach (Branch i in b) 150 | { 151 | branches.Add(i.FriendlyName); 152 | } 153 | 154 | found = true; 155 | break; 156 | } 157 | 158 | if (found) 159 | { 160 | break; 161 | } 162 | } 163 | } 164 | } 165 | catch (Exception ex) 166 | { 167 | this.locallogger.LogError("Bad - ", ex); 168 | } 169 | 170 | return branches; 171 | } 172 | 173 | public List SearchForCommit(MonitoredPathConfig monitoredPathConfig, string monitoredPathName, string sha) 174 | { 175 | List commits = new List(); 176 | 177 | try 178 | { 179 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 180 | { 181 | monitoredPathName = "default"; 182 | } 183 | 184 | foreach (MonitoredPath mp in monitoredPathConfig.MonitoredPaths) 185 | { 186 | if (string.Compare(mp.Name, monitoredPathName, StringComparison.OrdinalIgnoreCase) == 0 || monitoredPathName == "*") 187 | { 188 | DirectoryInfo[] directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 189 | foreach (DirectoryInfo dir in directoriesToScan) 190 | { 191 | try 192 | { 193 | using (Repository repo = new Repository(dir.FullName)) 194 | { 195 | try 196 | { 197 | Commit com = repo.Lookup(sha); 198 | if (com != null) 199 | { 200 | GitRepository gitrepo = new GitRepository(); 201 | foreach (var repo1 in mp.Repositories) 202 | { 203 | if (string.Compare(repo1.Name, dir.Name, StringComparison.OrdinalIgnoreCase) == 0) 204 | { 205 | gitrepo = new GitRepository 206 | { 207 | AllowFetch = repo1.AllowFetch, 208 | CommitUrl = repo1.CommitUrl, 209 | FriendlyName = string.IsNullOrWhiteSpace(repo1.FriendlyName) ? dir.Name : repo1.FriendlyName, 210 | Name = dir.Name 211 | }; 212 | } 213 | } 214 | 215 | string url = string.IsNullOrWhiteSpace(gitrepo.CommitUrl) ? string.Empty : string.Format($"{gitrepo.CommitUrl}{com.Sha}"); 216 | string repositoryUrl = string.Empty; 217 | if (repo.Network.Remotes?["origin"] != null) 218 | { 219 | repositoryUrl = repo.Network.Remotes["origin"].Url; 220 | } 221 | 222 | commits.Add(new GitCommit 223 | { 224 | Author = com.Author.Name, 225 | AuthorEmail = string.IsNullOrWhiteSpace(com.Author.Email) ? string.Empty : com.Author.Email, 226 | AuthorWhen = com.Author.When.UtcDateTime, 227 | Committer = com.Committer.Name, 228 | CommitterEmail = string.IsNullOrWhiteSpace(com.Committer.Email) ? string.Empty : com.Committer.Email, 229 | CommitterWhen = com.Committer.When.UtcDateTime, 230 | Sha = com.Sha, 231 | Message = com.Message, 232 | RepositoryFriendlyName = gitrepo.FriendlyName, 233 | RepositoryName = dir.Name, 234 | RepositoryUrl = repositoryUrl, 235 | CommitUrl = url, 236 | IsMerge = com.Parents.Count() > 1 237 | }); 238 | } 239 | } 240 | catch 241 | { 242 | // nothing 243 | } 244 | } 245 | } 246 | catch (Exception) 247 | { 248 | // do nothing 249 | } 250 | } 251 | } 252 | } 253 | 254 | return commits; 255 | } 256 | catch (Exception ex) 257 | { 258 | this.locallogger.LogError("Bad - ", ex); 259 | } 260 | 261 | return commits; 262 | } 263 | 264 | public void FetchAll(MonitoredPathConfig mpc, string monitoredPathName) 265 | { 266 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 267 | { 268 | monitoredPathName = "default"; 269 | } 270 | 271 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 272 | { 273 | if (mp.Name.ToLower() == monitoredPathName.ToLower()) 274 | { 275 | if (mp.AllowFetch) 276 | { 277 | try 278 | { 279 | DirectoryInfo[] directoriesToScan; 280 | if (mp.AllFolders) 281 | { 282 | directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 283 | } 284 | else 285 | { 286 | directoriesToScan = new DirectoryInfo[mp.Repositories.Count]; 287 | int i = 0; 288 | foreach (var dir in mp.Repositories) 289 | { 290 | directoriesToScan[i++] = new DirectoryInfo(Path.Combine(mp.Path, dir.Name)); 291 | } 292 | } 293 | 294 | foreach (var dir in directoriesToScan) 295 | { 296 | try 297 | { 298 | using (var repo = new Repository(dir.FullName)) 299 | { 300 | string logMessage = string.Empty; 301 | Remote remote = repo.Network.Remotes["origin"]; 302 | IEnumerable refSpecs = remote.FetchRefSpecs.Select(x => x.Specification); 303 | Commands.Fetch(repo, remote.Name, refSpecs, null, logMessage); 304 | 305 | this.locallogger.LogInformation($"{logMessage}"); 306 | } 307 | } 308 | catch (Exception ex) 309 | { 310 | this.locallogger.LogError($"Error: {ex.Message}"); 311 | 312 | // swallow 313 | } 314 | } 315 | } 316 | catch (Exception ex) 317 | { 318 | this.locallogger.LogError($"Error: {ex.Message}"); 319 | 320 | // swallow 321 | } 322 | } 323 | } 324 | } 325 | } 326 | 327 | public MonitoredPath Get(MonitoredPathConfig mpc, string monitoredPathName, string repoName, string branchName, string commitId) 328 | { 329 | MonitoredPath newmonitoredPath = new MonitoredPath(); 330 | try 331 | { 332 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 333 | { 334 | monitoredPathName = "default"; 335 | } 336 | 337 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 338 | { 339 | if (mp.Name.ToLower() == monitoredPathName.ToLower()) 340 | { 341 | DirectoryInfo[] directoriesToScan; 342 | 343 | if (!string.IsNullOrWhiteSpace(repoName)) 344 | { 345 | directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories(repoName, SearchOption.TopDirectoryOnly); 346 | } 347 | else 348 | { 349 | if (mp.AllFolders) 350 | { 351 | directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 352 | } 353 | else 354 | { 355 | directoriesToScan = new DirectoryInfo[mp.Repositories.Count]; 356 | int i = 0; 357 | foreach (var dir in mp.Repositories) 358 | { 359 | directoriesToScan[i++] = new DirectoryInfo(Path.Combine(mp.Path, dir.Name)); 360 | } 361 | } 362 | } 363 | 364 | newmonitoredPath = this.GetMonitoredPath(commitId, mp, directoriesToScan, branchName); 365 | } 366 | } 367 | } 368 | catch (Exception ex) 369 | { 370 | this.locallogger.LogError("Bad - ", ex); 371 | return null; 372 | } 373 | 374 | return newmonitoredPath; 375 | } 376 | 377 | private IEnumerable ListBranchesContaininingCommit(Repository repo, string commitSha, string filter) 378 | { 379 | if (string.IsNullOrEmpty(filter)) 380 | { 381 | foreach (var branch in repo.Branches) 382 | { 383 | var commits = branch.Commits.Where(c => c.Sha == commitSha); 384 | 385 | if (!commits.Any()) 386 | { 387 | continue; 388 | } 389 | 390 | yield return branch; 391 | } 392 | } 393 | else 394 | { 395 | foreach (var branch in repo.Branches.Where(n => n.FriendlyName.Contains(filter))) 396 | { 397 | var commits = branch.Commits.Where(c => c.Sha == commitSha); 398 | 399 | if (!commits.Any()) 400 | { 401 | continue; 402 | } 403 | 404 | yield return branch; 405 | } 406 | } 407 | } 408 | 409 | private MonitoredPath GetMonitoredPath(MonitoredPathConfig monitoredPathConfig, MonitoredPath monitoredPath, string repository, string branchName, DateTime startDateTime, DateTime endDateTime) 410 | { 411 | List commits = new List(); 412 | DirectoryInfo[] directoriesToScan; 413 | if (!string.IsNullOrWhiteSpace(repository)) 414 | { 415 | directoriesToScan = new DirectoryInfo(monitoredPath.Path).GetDirectories(repository, SearchOption.TopDirectoryOnly); 416 | } 417 | else 418 | { 419 | if (monitoredPath.AllFolders) 420 | { 421 | directoriesToScan = new DirectoryInfo(monitoredPath.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 422 | } 423 | else 424 | { 425 | directoriesToScan = new DirectoryInfo[monitoredPath.Repositories.Count]; 426 | int i = 0; 427 | foreach (var dir in monitoredPath.Repositories) 428 | { 429 | directoriesToScan[i++] = new DirectoryInfo(Path.Combine(monitoredPath.Path, dir.Name)); 430 | } 431 | } 432 | } 433 | 434 | MonitoredPath newmonitoredPath = new MonitoredPath(); 435 | foreach (var dir in directoriesToScan) 436 | { 437 | try 438 | { 439 | GitRepository gitrepo = this.TryGetRepo(monitoredPath, dir.Name); 440 | using (var repo = new Repository(dir.FullName)) 441 | { 442 | int commitCount = 0; 443 | if (string.IsNullOrEmpty(branchName)) 444 | { 445 | branchName = "master"; 446 | } 447 | 448 | string branch = repo.Info.IsBare ? branchName : $"origin/{branchName}"; 449 | gitrepo.Branch = branch; 450 | 451 | foreach (Commit com in repo.Branches[branch].Commits.Where(s => s.Committer.When >= startDateTime && s.Committer.When <= endDateTime).OrderByDescending(s => s.Author.When)) 452 | { 453 | if (!monitoredPath.IncludeMergeCommits) 454 | { 455 | // filter out merge commits 456 | if (com.Parents.Count() > 1) 457 | { 458 | continue; 459 | } 460 | } 461 | 462 | string[] nameexclusions = monitoredPathConfig.DefaultUserNameExcludeFilter.Split(','); 463 | if (nameexclusions.Any(name => com.Author.Name.Contains(name))) 464 | { 465 | continue; 466 | } 467 | 468 | string url = string.IsNullOrWhiteSpace(gitrepo.CommitUrl) ? string.Empty : string.Format($"{gitrepo.CommitUrl}{com.Sha}"); 469 | string repositoryUrl = string.Empty; 470 | if (repo.Network.Remotes?["origin"] != null) 471 | { 472 | repositoryUrl = repo.Network.Remotes["origin"].Url; 473 | } 474 | 475 | commits.Add(new GitCommit 476 | { 477 | Author = com.Author.Name, 478 | AuthorEmail = string.IsNullOrWhiteSpace(com.Author.Email) ? string.Empty : com.Author.Email, 479 | AuthorWhen = com.Author.When.UtcDateTime, 480 | Committer = com.Committer.Name, 481 | CommitterEmail = string.IsNullOrWhiteSpace(com.Committer.Email) ? string.Empty : com.Committer.Email, 482 | CommitterWhen = com.Committer.When.UtcDateTime, 483 | Sha = com.Sha, 484 | Message = com.Message, 485 | RepositoryFriendlyName = gitrepo.FriendlyName, 486 | RepositoryName = dir.Name, 487 | RepositoryUrl = repositoryUrl, 488 | CommitUrl = url, 489 | IsMerge = com.Parents.Count() > 1 490 | }); 491 | commitCount++; 492 | } 493 | 494 | gitrepo.CommitCount = commitCount; 495 | newmonitoredPath.Repositories.Add(gitrepo); 496 | newmonitoredPath.AllowFetch = monitoredPath.AllowFetch; 497 | } 498 | 499 | newmonitoredPath.Name = monitoredPath.Name; 500 | newmonitoredPath.AllowFetch = monitoredPath.AllowFetch; 501 | newmonitoredPath.AllFolders = monitoredPath.AllFolders; 502 | newmonitoredPath.StartDateTime = startDateTime; 503 | newmonitoredPath.EndDateTime = endDateTime; 504 | newmonitoredPath.Path = monitoredPath.Path; 505 | newmonitoredPath.CommitCount = commits.Count; 506 | newmonitoredPath.Commits = commits; 507 | newmonitoredPath.Commits.Sort((x, y) => -DateTime.Compare(x.CommitterWhen, y.CommitterWhen)); 508 | } 509 | catch (Exception ex) 510 | { 511 | this.locallogger.LogError("GetMonitoredItem Bad - ", ex); 512 | } 513 | } 514 | 515 | return newmonitoredPath; 516 | } 517 | 518 | private MonitoredPath GetMonitoredPath(MonitoredPathConfig monitoredPathConfig, MonitoredPath monitoredPath, string repository, string branchName, int days) 519 | { 520 | List commits = new List(); 521 | DirectoryInfo[] directoriesToScan; 522 | if (!string.IsNullOrWhiteSpace(repository)) 523 | { 524 | directoriesToScan = new DirectoryInfo(monitoredPath.Path).GetDirectories(repository, SearchOption.TopDirectoryOnly); 525 | } 526 | else 527 | { 528 | if (monitoredPath.AllFolders) 529 | { 530 | directoriesToScan = new DirectoryInfo(monitoredPath.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 531 | } 532 | else 533 | { 534 | directoriesToScan = new DirectoryInfo[monitoredPath.Repositories.Count]; 535 | int i = 0; 536 | foreach (var dir in monitoredPath.Repositories) 537 | { 538 | directoriesToScan[i++] = new DirectoryInfo(Path.Combine(monitoredPath.Path, dir.Name)); 539 | } 540 | } 541 | } 542 | 543 | if (days == 0) 544 | { 545 | days = monitoredPath.Days == 0 ? Convert.ToInt32(monitoredPathConfig.DefaultDays) : monitoredPath.Days; 546 | } 547 | 548 | if (days > 0) 549 | { 550 | days = days * -1; 551 | } 552 | 553 | MonitoredPath newmonitoredPath = new MonitoredPath(); 554 | foreach (var dir in directoriesToScan) 555 | { 556 | try 557 | { 558 | GitRepository gitrepo = this.TryGetRepo(monitoredPath, dir.Name); 559 | using (var repo = new Repository(dir.FullName)) 560 | { 561 | DateTime startDate = DateTime.Now.AddDays(days); 562 | int commitCount = 0; 563 | if (string.IsNullOrEmpty(branchName)) 564 | { 565 | branchName = "master"; 566 | } 567 | 568 | string branch = repo.Info.IsBare ? branchName : $"origin/{branchName}"; 569 | gitrepo.Branch = branch; 570 | foreach (Commit com in repo.Branches[branch].Commits.Where(s => s.Committer.When >= startDate).OrderByDescending(s => s.Author.When)) 571 | { 572 | if (!monitoredPath.IncludeMergeCommits) 573 | { 574 | // filter out merge commits 575 | if (com.Parents.Count() > 1) 576 | { 577 | continue; 578 | } 579 | } 580 | 581 | string[] nameexclusions = monitoredPathConfig.DefaultUserNameExcludeFilter.Split(','); 582 | if (nameexclusions.Any(name => com.Author.Name.Contains(name))) 583 | { 584 | continue; 585 | } 586 | 587 | string url = string.IsNullOrWhiteSpace(gitrepo.CommitUrl) ? string.Empty : string.Format($"{gitrepo.CommitUrl}{com.Sha}"); 588 | string repositoryUrl = string.Empty; 589 | if (repo.Network.Remotes?["origin"] != null) 590 | { 591 | repositoryUrl = repo.Network.Remotes["origin"].Url; 592 | } 593 | 594 | commits.Add(new GitCommit 595 | { 596 | Author = com.Author.Name, 597 | AuthorEmail = string.IsNullOrWhiteSpace(com.Author.Email) ? string.Empty : com.Author.Email, 598 | AuthorWhen = com.Author.When.UtcDateTime, 599 | Committer = com.Committer.Name, 600 | CommitterEmail = string.IsNullOrWhiteSpace(com.Committer.Email) ? string.Empty : com.Committer.Email, 601 | CommitterWhen = com.Committer.When.UtcDateTime, 602 | Sha = com.Sha, 603 | Message = com.Message, 604 | RepositoryFriendlyName = gitrepo.FriendlyName, 605 | RepositoryName = dir.Name, 606 | RepositoryUrl = repositoryUrl, 607 | CommitUrl = url, 608 | IsMerge = com.Parents.Count() > 1 609 | }); 610 | commitCount++; 611 | } 612 | 613 | gitrepo.CommitCount = commitCount; 614 | newmonitoredPath.Repositories.Add(gitrepo); 615 | newmonitoredPath.AllowFetch = monitoredPath.AllowFetch; 616 | } 617 | 618 | newmonitoredPath.Name = monitoredPath.Name; 619 | newmonitoredPath.AllowFetch = monitoredPath.AllowFetch; 620 | newmonitoredPath.AllFolders = monitoredPath.AllFolders; 621 | newmonitoredPath.Days = days; 622 | newmonitoredPath.Path = monitoredPath.Path; 623 | newmonitoredPath.CommitCount = commits.Count; 624 | newmonitoredPath.Commits = commits; 625 | newmonitoredPath.Commits.Sort((x, y) => -DateTime.Compare(x.CommitterWhen, y.CommitterWhen)); 626 | } 627 | catch (Exception ex) 628 | { 629 | this.locallogger.LogError("GetMonitoredItem Bad - ", ex); 630 | } 631 | } 632 | 633 | return newmonitoredPath; 634 | } 635 | 636 | private GitRepository TryGetRepo(MonitoredPath monitoredPath, string directoryName) 637 | { 638 | GitRepository r = new GitRepository { Name = directoryName, FriendlyName = directoryName }; 639 | if (monitoredPath.Repositories.Any()) 640 | { 641 | foreach (var repo in monitoredPath.Repositories) 642 | { 643 | if (string.Compare(repo.Name, directoryName, StringComparison.OrdinalIgnoreCase) == 0) 644 | { 645 | r = new GitRepository 646 | { 647 | AllowFetch = repo.AllowFetch, CommitUrl = repo.CommitUrl, 648 | FriendlyName = string.IsNullOrWhiteSpace(repo.FriendlyName) ? directoryName : repo.FriendlyName, 649 | Name = directoryName 650 | }; 651 | } 652 | } 653 | } 654 | 655 | return r; 656 | } 657 | 658 | private MonitoredPath GetMonitoredPath(string commitId, MonitoredPath monitoredPath, DirectoryInfo[] directoriesToScan, string branchName) 659 | { 660 | MonitoredPath newmonitoredPath = new MonitoredPath(); 661 | List commits = new List(); 662 | foreach (DirectoryInfo dir in directoriesToScan) 663 | { 664 | try 665 | { 666 | GitRepository gitrepo = this.TryGetRepo(monitoredPath, dir.Name); 667 | using (Repository repo = new Repository(dir.FullName)) 668 | { 669 | try 670 | { 671 | string branch = repo.Info.IsBare ? branchName : $"origin/{branchName}"; 672 | gitrepo.Branch = branch; 673 | 674 | int commitCount = 0; 675 | Commit com = repo.Lookup(commitId); 676 | 677 | if (com != null) 678 | { 679 | var comFilter = new CommitFilter 680 | { 681 | IncludeReachableFrom = branch, 682 | ExcludeReachableFrom = com 683 | }; 684 | 685 | var coms = repo.Commits.QueryBy(comFilter).OrderBy(s => s.Author.When); 686 | 687 | string repositoryUrl = string.Empty; 688 | if (repo.Network.Remotes?["origin"] != null) 689 | { 690 | repositoryUrl = repo.Network.Remotes["origin"].Url; 691 | } 692 | 693 | foreach (Commit cm in coms) 694 | { 695 | if (!monitoredPath.IncludeMergeCommits) 696 | { 697 | // filter out merge commits 698 | if (com.Parents.Count() > 1) 699 | { 700 | continue; 701 | } 702 | } 703 | 704 | commits.Add(new GitCommit 705 | { 706 | Author = cm.Author.Name, 707 | AuthorEmail = string.IsNullOrWhiteSpace(com.Author.Email) ? string.Empty : cm.Author.Email, 708 | AuthorWhen = cm.Author.When.UtcDateTime, 709 | Committer = cm.Committer.Name, 710 | CommitterEmail = string.IsNullOrWhiteSpace(com.Committer.Email) ? string.Empty : cm.Committer.Email, 711 | CommitterWhen = cm.Committer.When.UtcDateTime, 712 | Sha = cm.Sha, 713 | Message = cm.Message, 714 | RepositoryFriendlyName = gitrepo.FriendlyName, 715 | RepositoryName = dir.Name, 716 | RepositoryUrl = repositoryUrl, 717 | CommitUrl = string.IsNullOrWhiteSpace(gitrepo.CommitUrl) ? string.Empty : string.Format($"{gitrepo.CommitUrl}{cm.Sha}"), 718 | IsMerge = com.Parents.Count() > 1 719 | }); 720 | commitCount++; 721 | } 722 | 723 | gitrepo.CommitCount = commitCount; 724 | newmonitoredPath.Repositories.Add(gitrepo); 725 | } 726 | } 727 | catch (Exception ex) 728 | { 729 | this.locallogger.LogError("GetMonitoredPath Bad - ", ex); 730 | } 731 | 732 | newmonitoredPath.Name = monitoredPath.Name; 733 | newmonitoredPath.AllowFetch = monitoredPath.AllowFetch; 734 | newmonitoredPath.AllFolders = monitoredPath.AllFolders; 735 | newmonitoredPath.Path = monitoredPath.Path; 736 | newmonitoredPath.CommitCount = commits.Count; 737 | newmonitoredPath.Commits = commits; 738 | } 739 | } 740 | catch (Exception ex) 741 | { 742 | this.locallogger.LogError("GetMonitoredPath Bad - ", ex); 743 | } 744 | } 745 | 746 | return newmonitoredPath; 747 | } 748 | } 749 | } -------------------------------------------------------------------------------- /src/GitMonitor/Repositories/Interfaces/ICommitRepository.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Repositories 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using GitMonitor.Models; 9 | 10 | public interface ICommitRepository 11 | { 12 | void FetchAll(MonitoredPathConfig m, string name); 13 | 14 | List SearchForCommit(MonitoredPathConfig mpc, string repositoryName, string sha); 15 | 16 | List SearchBranchesForCommit(MonitoredPathConfig mpc, string repositoryName, string sha, string filter); 17 | 18 | MonitoredPathConfig Get(MonitoredPathConfig mpc, string name, string branchName, int days); 19 | 20 | MonitoredPathConfig Get(MonitoredPathConfig mpc, string name, string branchName, DateTime startDateTime, DateTime endDateTime); 21 | 22 | MonitoredPath Get(MonitoredPathConfig mpc, string monitoredPathName, string repoName, string branchName, int days); 23 | 24 | MonitoredPath Get(MonitoredPathConfig mpc, string monitoredPathName, string repoName, string branchName, DateTime startDateTime, DateTime endDateTime); 25 | 26 | MonitoredPath Get(MonitoredPathConfig mpc, string monitoredPathName, string repoName, string branchName, string commitId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/GitMonitor/Repositories/Interfaces/IRepoRepository.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Repositories 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using GitMonitor.Models; 9 | 10 | public interface IRepoRepository 11 | { 12 | List Get(MonitoredPathConfig mpc, string monitoredPathName, string repository); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/GitMonitor/Repositories/RepoRepository.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor.Repositories 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using GitMonitor.Models; 11 | using LibGit2Sharp; 12 | using Microsoft.Extensions.Logging; 13 | 14 | public class RepoRepository : IRepoRepository 15 | { 16 | private readonly ILogger locallogger; 17 | 18 | public RepoRepository(ILogger logger) 19 | { 20 | this.locallogger = logger; 21 | } 22 | 23 | public List Get(MonitoredPathConfig mpc, string monitoredPathName, string repository) 24 | { 25 | List repos = new List(); 26 | 27 | if (string.IsNullOrWhiteSpace(monitoredPathName)) 28 | { 29 | monitoredPathName = "default"; 30 | } 31 | 32 | foreach (MonitoredPath mp in mpc.MonitoredPaths) 33 | { 34 | if (string.Compare(mp.Name, monitoredPathName, StringComparison.OrdinalIgnoreCase) == 0) 35 | { 36 | DirectoryInfo[] directoriesToScan; 37 | if (!string.IsNullOrWhiteSpace(repository)) 38 | { 39 | directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories(repository, SearchOption.TopDirectoryOnly); 40 | } 41 | else 42 | { 43 | if (mp.AllFolders) 44 | { 45 | directoriesToScan = new DirectoryInfo(mp.Path).GetDirectories("*", SearchOption.TopDirectoryOnly); 46 | } 47 | else 48 | { 49 | directoriesToScan = new DirectoryInfo[mp.Repositories.Count]; 50 | int i = 0; 51 | foreach (var dir in mp.Repositories) 52 | { 53 | directoriesToScan[i++] = new DirectoryInfo(Path.Combine(mp.Path, dir.Name)); 54 | } 55 | } 56 | } 57 | 58 | foreach (var dir in directoriesToScan) 59 | { 60 | try 61 | { 62 | GitRepository gitrepo = this.TryGetRepo(mp, dir.Name); 63 | using (var repo = new Repository(dir.FullName)) 64 | { 65 | gitrepo.BranchCount = repo.Branches.Count(); 66 | Commit com = repo.Head.Tip; 67 | string url = string.IsNullOrWhiteSpace(gitrepo.CommitUrl) ? string.Empty : string.Format($"{gitrepo.CommitUrl}{com.Sha}"); 68 | 69 | string repositoryUrl = string.Empty; 70 | 71 | if (repo.Network.Remotes?["origin"] != null) 72 | { 73 | repositoryUrl = repo.Network.Remotes["origin"].Url; 74 | } 75 | 76 | gitrepo.LastCommit = new GitCommit 77 | { 78 | Author = com.Author.Name, 79 | AuthorEmail = string.IsNullOrWhiteSpace(com.Author.Email) ? string.Empty : com.Author.Email, 80 | AuthorWhen = com.Author.When.UtcDateTime, 81 | Committer = com.Committer.Name, 82 | CommitterEmail = string.IsNullOrWhiteSpace(com.Committer.Email) ? string.Empty : com.Committer.Email, 83 | CommitterWhen = com.Committer.When.UtcDateTime, 84 | Sha = com.Sha, 85 | Message = com.Message, 86 | RepositoryFriendlyName = gitrepo.FriendlyName, 87 | RepositoryName = dir.Name, 88 | RepositoryUrl = repositoryUrl, 89 | CommitUrl = url, 90 | IsMerge = com.Parents.Count() > 1 91 | }; 92 | 93 | repos.Add(gitrepo); 94 | } 95 | } 96 | catch (Exception ex) 97 | { 98 | this.locallogger.LogError("GetMonitoredItem Bad - ", ex); 99 | } 100 | } 101 | } 102 | } 103 | 104 | return repos; 105 | } 106 | 107 | private GitRepository TryGetRepo(MonitoredPath monitoredPath, string directoryName) 108 | { 109 | GitRepository r = new GitRepository { Name = directoryName, FriendlyName = directoryName }; 110 | if (monitoredPath.Repositories.Any()) 111 | { 112 | foreach (var repo in monitoredPath.Repositories) 113 | { 114 | if (string.Compare(repo.Name, directoryName, StringComparison.OrdinalIgnoreCase) == 0) 115 | { 116 | r = new GitRepository 117 | { 118 | AllowFetch = repo.AllowFetch, CommitUrl = repo.CommitUrl, 119 | FriendlyName = string.IsNullOrWhiteSpace(repo.FriendlyName) ? directoryName : repo.FriendlyName, 120 | Name = directoryName 121 | }; 122 | } 123 | } 124 | } 125 | 126 | return r; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/GitMonitor/Startup.cs: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------------------------------------------------------------------------- 2 | // (c) 2018 Mike Fourie and Contributors (https://github.com/mikefourie/GitMonitor) under MIT License. See https://opensource.org/licenses/MIT 3 | // -------------------------------------------------------------------------------------------------------------------- 4 | namespace GitMonitor 5 | { 6 | using GitMonitor.Models; 7 | using GitMonitor.Repositories; 8 | using Microsoft.AspNetCore.Builder; 9 | using Microsoft.AspNetCore.Hosting; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Newtonsoft.Json.Serialization; 14 | 15 | public class Startup 16 | { 17 | public Startup(IHostingEnvironment env) 18 | { 19 | // Set up configuration sources. 20 | var builder = new ConfigurationBuilder() 21 | .SetBasePath(env.ContentRootPath) 22 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 23 | .AddJsonFile("gitmonitorconfig.json", optional: true, reloadOnChange: true); 24 | 25 | builder.AddEnvironmentVariables(); 26 | this.Configuration = builder.Build(); 27 | } 28 | 29 | public IConfigurationRoot Configuration { get; } 30 | 31 | // This method gets called by the runtime. Use this method to add services to the container. 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | services.AddMvc() 35 | .AddJsonOptions(opt => 36 | { 37 | opt.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); 38 | }); 39 | services.AddLogging(); 40 | 41 | // Add application services. 42 | services.AddScoped(); 43 | services.AddScoped(); 44 | services.Configure(x => this.Configuration.GetSection("MonitoredPathConfig").Bind(x)); 45 | } 46 | 47 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 48 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 49 | { 50 | loggerFactory.AddConsole(this.Configuration.GetSection("Logging")); 51 | loggerFactory.AddDebug(LogLevel.Information); 52 | 53 | if (env.IsDevelopment()) 54 | { 55 | app.UseDeveloperExceptionPage(); 56 | app.UseDatabaseErrorPage(); 57 | app.UseBrowserLink(); 58 | } 59 | else 60 | { 61 | app.UseExceptionHandler("/Home/Error"); 62 | } 63 | 64 | app.UseStaticFiles(); 65 | app.UseMvc(routes => 66 | { 67 | routes.MapRoute( 68 | name: "default", 69 | template: "{controller=Home}/{action=Index}/{id?}"); 70 | }); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/GitMonitor/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using System 2 | @using System.Text.Encodings.Web 3 | @using System.Threading.Tasks 4 | @using Microsoft.CodeAnalysis.CSharp.Syntax 5 | @using Microsoft.EntityFrameworkCore.Metadata.Internal 6 | @model GitMonitor.Models.MonitoredPathConfig 7 |
8 |
9 |
10 |
11 | @if (Model != null) 12 | { 13 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | @foreach (var commit in @Model.ActiveMonitoredPath.Commits) 38 | { 39 | 40 | 43 | 44 | @if (@commit.CommitterWhen.ToString("dd MMM yyy") == DateTime.Now.ToString("dd MMM yyy")) 45 | { 46 | 47 | } 48 | else if (@commit.CommitterWhen > DateTime.UtcNow.AddDays(-7)) 49 | { 50 | 51 | } 52 | else 53 | { 54 | 55 | } 56 | 57 | @if (string.IsNullOrWhiteSpace(@commit.CommitUrl)) 58 | { 59 | 60 | } 61 | else 62 | { 63 | 64 | } 65 | @if (@commit.IsMerge) 66 | { 67 | 70 | } 71 | else 72 | { 73 | 74 | } 75 | 76 | } 77 | 78 |
RepositoryDateUserShaMessage
41 | 42 | @commit.RepositoryFriendlyName@commit.CommitterWhen.ToString("HH:mm") @commit.CommitterWhen.ToString("ddd HH:mm") @commit.CommitterWhen.ToString("dd MMM yy") @commit.Author@commit.Sha@commit.Sha 68 |

@commit.Message

69 |
@commit.Message
79 |
80 | @foreach (var mp in @Model.ActiveMonitoredPath.Repositories) 81 | { 82 | if (mp.CommitCount > 0) 83 | { 84 |
85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | @foreach (var commit in @Model.ActiveMonitoredPath.Commits) 98 | { 99 | if (@mp.FriendlyName == @commit.RepositoryFriendlyName) 100 | { 101 | 102 | 105 | 106 | @if (@commit.CommitterWhen.ToString("dd MMM yyy") == DateTime.Now.ToString("dd MMM yyy")) 107 | { 108 | 109 | } 110 | else if (@commit.CommitterWhen > DateTime.UtcNow.AddDays(-7)) 111 | { 112 | 113 | } 114 | else 115 | { 116 | 117 | } 118 | 119 | @if (string.IsNullOrWhiteSpace(@commit.CommitUrl)) 120 | { 121 | 122 | } 123 | else 124 | { 125 | 126 | } 127 | @if (@commit.IsMerge) 128 | { 129 | 132 | } 133 | else 134 | { 135 | 136 | } 137 | 138 | } 139 | } 140 | 141 |
RepositoryDateUserShaMessage
103 | 104 | @commit.RepositoryFriendlyName@commit.CommitterWhen.ToString("HH:mm") @commit.CommitterWhen.ToString("ddd HH:mm") @commit.CommitterWhen.ToString("dd MMM yy") @commit.Author@commit.Sha@commit.Sha 130 |

@commit.Message

131 |
@commit.Message
142 |
143 | } 144 | } 145 |
146 | } 147 |
148 |
149 | 150 | 151 | 170 |
171 | 172 | 173 | @* 174 | 175 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 |
@commit.RepositoryName - 176 | 177 | @if (string.IsNullOrWhiteSpace(@commit.CommitUrl)) 178 | { 179 | @commit.Sha 180 | } 181 | else 182 | { 183 | @commit.Sha 184 | } 185 | 186 |
@commit.Author @commit.AuthorEmail @commit.CommitterWhen
@commit.Message
*@ 195 | 196 | 197 | @section scripts{ 198 | 239 | } -------------------------------------------------------------------------------- /src/GitMonitor/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | -------------------------------------------------------------------------------- /src/GitMonitor/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @model GitMonitor.Models.MonitoredPathConfig 2 | 3 | 4 | 5 | 6 | Git Repository Monitor 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 82 |
83 |
84 | @RenderBody() 85 |
86 |
87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | @RenderSection("scripts", required: false) 98 | 99 | -------------------------------------------------------------------------------- /src/GitMonitor/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using GitMonitor 2 | @using GitMonitor.Models 3 | @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" -------------------------------------------------------------------------------- /src/GitMonitor/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/GitMonitor/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/GitMonitor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/GitMonitor/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asp.net", 3 | "private": true, 4 | "dependencies": { 5 | "font-awesome": "4.7.0", 6 | "jquery": "3.2.1", 7 | "jquery-validation": "1.17.0", 8 | "jquery-validation-unobtrusive": "3.2.6", 9 | "popper.js": "v1.12.5" 10 | }, 11 | "resolutions": { 12 | "jquery": "3.2.1", 13 | "jquery-validation": "1.17.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/GitMonitor/gitmonitorconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "MonitoredPathConfig": { 3 | "DefaultDays": "-3", 4 | "DefaultRemote": "origin", 5 | "DefaultBranch": "master", 6 | "DefaultUserNameExcludeFilter": "ServiceUser", 7 | "MonitoredPaths": [ 8 | { 9 | "Path": "c:\\gitcode", 10 | "Name": "Default", 11 | "AllowFetch": true, 12 | "AllFolders": true 13 | }, 14 | { 15 | "Path": "c:\\gitcode\\almrangers", 16 | "Name": "alm rangers", 17 | "AllFolders": true, 18 | "AllowFetch": true, 19 | "Days": "-10", 20 | "IncludeMergeCommits": true, 21 | "Repositories": [ 22 | { 23 | "Name": "VSO-Extension-FolderManagement", 24 | "CommitUrl": "https://github.com/ALM-Rangers/VSO-Extension-FolderManagement/commit/", 25 | "Remote": "origin", 26 | "Branch": "master", 27 | "FriendlyName": "VSO-Ext-Fol" 28 | }, 29 | 30 | { 31 | "Name": "Extracting-effective-permissions-from-TFS", 32 | "CommitUrl": "https://github.com/ALM-Rangers/Extracting-effective-permissions-from-TFS/commit/", 33 | "Remote": "origin", 34 | "Branch": "master" 35 | }, 36 | { 37 | "Name": "VSTS-Extension-PrintCards", 38 | "CommitUrl": "https://github.com/ALM-Rangers/VSTS-Extension-PrintCards/commit/", 39 | "Remote": "origin", 40 | "Branch": "master" 41 | }, 42 | { 43 | "Name": "Migrate-assets-from-RM-server-to-VSTS", 44 | "CommitUrl": "https://github.com/ALM-Rangers/Migrate-assets-from-RM-server-to-VSTS/commit/", 45 | "Remote": "origin", 46 | "Branch": "master" 47 | }, 48 | { 49 | "Name": "Template", 50 | "CommitUrl": "https://github.com/ALM-Rangers/Template/commit/", 51 | "Remote": "origin", 52 | "Branch": "master" 53 | } 54 | ] 55 | }, 56 | { 57 | "Path": "c:\\gitcode\\githubtrack", 58 | "Name": "github track", 59 | "AllFolders": true, 60 | "AllowFetch": true, 61 | "Days": "-10", 62 | "Repositories": [ 63 | { 64 | "Name": "ilspy", 65 | "CommitUrl": "https://github.com/icsharpcode/ILSpy/commit/", 66 | "Remote": "origin", 67 | "Branch": "master" 68 | }, 69 | { 70 | "Name": "libgit2sharp", 71 | "CommitUrl": "https://github.com/libgit2/libgit2sharp/commit/", 72 | "Remote": "origin", 73 | "Branch": "master" 74 | } 75 | ] 76 | }, 77 | { 78 | "Path": "c:\\gitcode\\work", 79 | "Name": "work", 80 | "AllFolders": true 81 | } 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/GitMonitor/gulpfile.js: -------------------------------------------------------------------------------- 1 | /// 2 | "use strict"; 3 | 4 | var gulp = require("gulp"), 5 | rimraf = require("rimraf"), 6 | concat = require("gulp-concat"), 7 | cssmin = require("gulp-cssmin"), 8 | uglify = require("gulp-uglify"); 9 | 10 | var webroot = "./wwwroot/"; 11 | 12 | var paths = { 13 | js: webroot + "js/**/*.js", 14 | minJs: webroot + "js/**/*.min.js", 15 | css: webroot + "css/**/*.css", 16 | minCss: webroot + "css/**/*.min.css", 17 | concatJsDest: webroot + "js/site.min.js", 18 | concatCssDest: webroot + "css/site.min.css" 19 | }; 20 | 21 | gulp.task("clean:js", function (cb) { 22 | rimraf(paths.concatJsDest, cb); 23 | }); 24 | 25 | gulp.task("clean:css", function (cb) { 26 | rimraf(paths.concatCssDest, cb); 27 | }); 28 | 29 | gulp.task("clean", ["clean:js", "clean:css"]); 30 | 31 | gulp.task("min:js", function () { 32 | return gulp.src([paths.js, "!" + paths.minJs], { base: "." }) 33 | .pipe(concat(paths.concatJsDest)) 34 | .pipe(uglify()) 35 | .pipe(gulp.dest(".")); 36 | }); 37 | 38 | gulp.task("min:css", function () { 39 | return gulp.src([paths.css, "!" + paths.minCss]) 40 | .pipe(concat(paths.concatCssDest)) 41 | .pipe(cssmin()) 42 | .pipe(gulp.dest(".")); 43 | }); 44 | 45 | gulp.task("min", ["min:js", "min:css"]); 46 | -------------------------------------------------------------------------------- /src/GitMonitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ASP.NET", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "gulp": "3.8.11", 6 | "gulp-concat": "2.5.2", 7 | "gulp-cssmin": "0.1.7", 8 | "gulp-uglify": "1.2.0", 9 | "rimraf": "2.2.8" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/GitMonitor/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* site.css */ 2 | /*.navbar { 3 | background: #0069AA; 4 | }*/ 5 | .menu { 6 | font-size: 12px; 7 | } 8 | 9 | .menu li { 10 | list-style-type: none; 11 | } 12 | 13 | .menu li.active { 14 | font-weight: bold; 15 | } 16 | 17 | .menu li a { 18 | color: white; 19 | } 20 | 21 | .floatleft { float:left } 22 | 23 | .floatright { float:right } 24 | 25 | .table-sm{ 26 | font-size: 12px; 27 | } -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | .menu{font-size:12px}.menu li{list-style-type:none}.menu li.active{font-weight:700}.menu li a{color:#fff} -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikefourie-zz/GitMonitor/28e6f64b6dc5e8de1d38faf768f848e6debda7e9/src/GitMonitor/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | if (location.hash) { 3 | $('a[href=' + location.hash + ']').tab('show'); 4 | } 5 | $(document.body).on("click", "a[data-toggle]", function (event) { 6 | location.hash = this.getAttribute("href"); 7 | }); 8 | }); 9 | $(window).on('popstate', function () { 10 | var anchor = location.hash || $("a[data-toggle=tab]").first().attr("href"); 11 | $('a[href=' + anchor + ']').tab('show'); 12 | }); 13 | /* 14 | var timeout = setTimeout("location.reload(true);", 120000); 15 | function resetTimeout() { 16 | clearTimeout(timeout); 17 | timeout = setTimeout("location.reload(true);", 120000); 18 | }*/ -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){location.hash&&$("a[href="+location.hash+"]").tab("show"),$(document.body).on("click","a[data-toggle]",function(t){location.hash=this.getAttribute("href")})}),$(window).on("popstate",function(){var t=location.hash||$("a[data-toggle=tab]").first().attr("href");$("a[href="+t+"]").tab("show")}); -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Grid v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | */@-ms-viewport{width:device-width}html{box-sizing:border-box;-ms-overflow-style:scrollbar}*,::after,::before{box-sizing:inherit}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-webkit-inline-box!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}} 7 | /*# sourceMappingURL=bootstrap-grid.min.css.map */ -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | line-height: 1.5; 37 | color: #212529; 38 | text-align: left; 39 | background-color: #fff; 40 | } 41 | 42 | [tabindex="-1"]:focus { 43 | outline: 0 !important; 44 | } 45 | 46 | hr { 47 | box-sizing: content-box; 48 | height: 0; 49 | overflow: visible; 50 | } 51 | 52 | h1, h2, h3, h4, h5, h6 { 53 | margin-top: 0; 54 | margin-bottom: 0.5rem; 55 | } 56 | 57 | p { 58 | margin-top: 0; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | abbr[title], 63 | abbr[data-original-title] { 64 | text-decoration: underline; 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | cursor: help; 68 | border-bottom: 0; 69 | } 70 | 71 | address { 72 | margin-bottom: 1rem; 73 | font-style: normal; 74 | line-height: inherit; 75 | } 76 | 77 | ol, 78 | ul, 79 | dl { 80 | margin-top: 0; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | ol ol, 85 | ul ul, 86 | ol ul, 87 | ul ol { 88 | margin-bottom: 0; 89 | } 90 | 91 | dt { 92 | font-weight: 700; 93 | } 94 | 95 | dd { 96 | margin-bottom: .5rem; 97 | margin-left: 0; 98 | } 99 | 100 | blockquote { 101 | margin: 0 0 1rem; 102 | } 103 | 104 | dfn { 105 | font-style: italic; 106 | } 107 | 108 | b, 109 | strong { 110 | font-weight: bolder; 111 | } 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | sub, 118 | sup { 119 | position: relative; 120 | font-size: 75%; 121 | line-height: 0; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -.25em; 127 | } 128 | 129 | sup { 130 | top: -.5em; 131 | } 132 | 133 | a { 134 | color: #007bff; 135 | text-decoration: none; 136 | background-color: transparent; 137 | -webkit-text-decoration-skip: objects; 138 | } 139 | 140 | a:hover { 141 | color: #0056b3; 142 | text-decoration: underline; 143 | } 144 | 145 | a:not([href]):not([tabindex]) { 146 | color: inherit; 147 | text-decoration: none; 148 | } 149 | 150 | a:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus { 151 | color: inherit; 152 | text-decoration: none; 153 | } 154 | 155 | a:not([href]):not([tabindex]):focus { 156 | outline: 0; 157 | } 158 | 159 | pre, 160 | code, 161 | kbd, 162 | samp { 163 | font-family: monospace, monospace; 164 | font-size: 1em; 165 | } 166 | 167 | pre { 168 | margin-top: 0; 169 | margin-bottom: 1rem; 170 | overflow: auto; 171 | -ms-overflow-style: scrollbar; 172 | } 173 | 174 | figure { 175 | margin: 0 0 1rem; 176 | } 177 | 178 | img { 179 | vertical-align: middle; 180 | border-style: none; 181 | } 182 | 183 | svg:not(:root) { 184 | overflow: hidden; 185 | } 186 | 187 | table { 188 | border-collapse: collapse; 189 | } 190 | 191 | caption { 192 | padding-top: 0.75rem; 193 | padding-bottom: 0.75rem; 194 | color: #6c757d; 195 | text-align: left; 196 | caption-side: bottom; 197 | } 198 | 199 | th { 200 | text-align: inherit; 201 | } 202 | 203 | label { 204 | display: inline-block; 205 | margin-bottom: .5rem; 206 | } 207 | 208 | button { 209 | border-radius: 0; 210 | } 211 | 212 | button:focus { 213 | outline: 1px dotted; 214 | outline: 5px auto -webkit-focus-ring-color; 215 | } 216 | 217 | input, 218 | button, 219 | select, 220 | optgroup, 221 | textarea { 222 | margin: 0; 223 | font-family: inherit; 224 | font-size: inherit; 225 | line-height: inherit; 226 | } 227 | 228 | button, 229 | input { 230 | overflow: visible; 231 | } 232 | 233 | button, 234 | select { 235 | text-transform: none; 236 | } 237 | 238 | button, 239 | html [type="button"], 240 | [type="reset"], 241 | [type="submit"] { 242 | -webkit-appearance: button; 243 | } 244 | 245 | button::-moz-focus-inner, 246 | [type="button"]::-moz-focus-inner, 247 | [type="reset"]::-moz-focus-inner, 248 | [type="submit"]::-moz-focus-inner { 249 | padding: 0; 250 | border-style: none; 251 | } 252 | 253 | input[type="radio"], 254 | input[type="checkbox"] { 255 | box-sizing: border-box; 256 | padding: 0; 257 | } 258 | 259 | input[type="date"], 260 | input[type="time"], 261 | input[type="datetime-local"], 262 | input[type="month"] { 263 | -webkit-appearance: listbox; 264 | } 265 | 266 | textarea { 267 | overflow: auto; 268 | resize: vertical; 269 | } 270 | 271 | fieldset { 272 | min-width: 0; 273 | padding: 0; 274 | margin: 0; 275 | border: 0; 276 | } 277 | 278 | legend { 279 | display: block; 280 | width: 100%; 281 | max-width: 100%; 282 | padding: 0; 283 | margin-bottom: .5rem; 284 | font-size: 1.5rem; 285 | line-height: inherit; 286 | color: inherit; 287 | white-space: normal; 288 | } 289 | 290 | progress { 291 | vertical-align: baseline; 292 | } 293 | 294 | [type="number"]::-webkit-inner-spin-button, 295 | [type="number"]::-webkit-outer-spin-button { 296 | height: auto; 297 | } 298 | 299 | [type="search"] { 300 | outline-offset: -2px; 301 | -webkit-appearance: none; 302 | } 303 | 304 | [type="search"]::-webkit-search-cancel-button, 305 | [type="search"]::-webkit-search-decoration { 306 | -webkit-appearance: none; 307 | } 308 | 309 | ::-webkit-file-upload-button { 310 | font: inherit; 311 | -webkit-appearance: button; 312 | } 313 | 314 | output { 315 | display: inline-block; 316 | } 317 | 318 | summary { 319 | display: list-item; 320 | cursor: pointer; 321 | } 322 | 323 | template { 324 | display: none; 325 | } 326 | 327 | [hidden] { 328 | display: none !important; 329 | } 330 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0 (https://getbootstrap.com) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/GitMonitor/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../../scss/bootstrap-reboot.scss","../../scss/_reboot.scss","dist/css/bootstrap-reboot.css","bootstrap-reboot.css","../../scss/mixins/_hover.scss"],"names":[],"mappings":"AAAA;;;;;;ACoBA,ECXA,QADA,SDeE,WAAA,WAGF,KACE,YAAA,WACA,YAAA,KACA,yBAAA,KACA,qBAAA,KACA,mBAAA,UACA,4BAAA,YAKA,cACE,MAAA,aAMJ,QAAA,MAAA,OAAA,WAAA,OAAA,OAAA,OAAA,OAAA,KAAA,IAAA,QACE,QAAA,MAWF,KACE,OAAA,EACA,YAAA,aAAA,CAAA,kBAAA,CAAA,UAAA,CAAA,MAAA,CAAA,gBAAA,CAAA,KAAA,CAAA,UAAA,CAAA,mBAAA,CAAA,gBAAA,CAAA,kBACA,UAAA,KACA,YAAA,IACA,YAAA,IACA,MAAA,QACA,WAAA,KACA,iBAAA,KEvBF,sBFgCE,QAAA,YASF,GACE,WAAA,YACA,OAAA,EACA,SAAA,QAaF,GAAA,GAAA,GAAA,GAAA,GAAA,GACE,WAAA,EACA,cAAA,MAQF,EACE,WAAA,EACA,cAAA,KChDF,0BD0DA,YAEE,gBAAA,UACA,wBAAA,UAAA,OAAA,gBAAA,UAAA,OACA,OAAA,KACA,cAAA,EAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QCrDF,GDwDA,GCzDA,GD4DE,WAAA,EACA,cAAA,KAGF,MCxDA,MACA,MAFA,MD6DE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAGF,IACE,WAAA,OAIF,EC1DA,OD4DE,YAAA,OAIF,MACE,UAAA,IAQF,IChEA,IDkEE,SAAA,SACA,UAAA,IACA,YAAA,EACA,eAAA,SAGF,IAAM,OAAA,OACN,IAAM,IAAA,MAON,EACE,MAAA,QACA,gBAAA,KACA,iBAAA,YACA,6BAAA,QG3LA,QH8LE,MAAA,QACA,gBAAA,UAUJ,8BACE,MAAA,QACA,gBAAA,KGvMA,oCAAA,oCH0ME,MAAA,QACA,gBAAA,KANJ,oCAUI,QAAA,EClEJ,KACA,ID2EA,IC1EA,KD8EE,YAAA,SAAA,CAAA,UACA,UAAA,IAIF,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAGA,mBAAA,UAQF,OAEE,OAAA,EAAA,EAAA,KAQF,IACE,eAAA,OACA,aAAA,KAGF,eACE,SAAA,OAQF,MACE,gBAAA,SAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAGE,WAAA,QAQF,MAEE,QAAA,aACA,cAAA,MAMF,OACE,cAAA,EAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBC9GF,ODiHA,MC/GA,SADA,OAEA,SDmHE,OAAA,EACA,YAAA,QACA,UAAA,QACA,YAAA,QAGF,OCjHA,MDmHE,SAAA,QAGF,OCjHA,ODmHE,eAAA,KC7GF,aACA,cDkHA,OCpHA,mBDwHE,mBAAA,OCjHF,gCACA,+BACA,gCDmHA,yBAIE,QAAA,EACA,aAAA,KClHF,qBDqHA,kBAEE,WAAA,WACA,QAAA,EAIF,iBCrHA,2BACA,kBAFA,iBD+HE,mBAAA,QAGF,SACE,SAAA,KAEA,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAKF,OACE,QAAA,MACA,MAAA,KACA,UAAA,KACA,QAAA,EACA,cAAA,MACA,UAAA,OACA,YAAA,QACA,MAAA,QACA,YAAA,OAGF,SACE,eAAA,SEnIF,yCDEA,yCDuIE,OAAA,KEpIF,cF4IE,eAAA,KACA,mBAAA,KExIF,4CDEA,yCD+IE,mBAAA,KAQF,6BACE,KAAA,QACA,mBAAA,OAOF,OACE,QAAA,aAGF,QACE,QAAA,UACA,OAAA,QAGF,SACE,QAAA,KErJF,SF2JE,QAAA","sourcesContent":["/*!\n * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n\n@import \"functions\";\n@import \"variables\";\n@import \"mixins\";\n@import \"reboot\";\n","// stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix\n\n// Reboot\n//\n// Normalization of HTML elements, manually forked from Normalize.css to remove\n// styles targeting irrelevant browsers while applying new styles.\n//\n// Normalize is licensed MIT. https://github.com/necolas/normalize.css\n\n\n// Document\n//\n// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.\n// 2. Change the default font family in all browsers.\n// 3. Correct the line height in all browsers.\n// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.\n// 5. Setting @viewport causes scrollbars to overlap content in IE11 and Edge, so\n// we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n// 6. Change the default tap highlight to be completely transparent in iOS.\n\n*,\n*::before,\n*::after {\n box-sizing: border-box; // 1\n}\n\nhtml {\n font-family: sans-serif; // 2\n line-height: 1.15; // 3\n -webkit-text-size-adjust: 100%; // 4\n -ms-text-size-adjust: 100%; // 4\n -ms-overflow-style: scrollbar; // 5\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0); // 6\n}\n\n// IE10+ doesn't honor `` in some cases.\n@at-root {\n @-ms-viewport {\n width: device-width;\n }\n}\n\n// stylelint-disable selector-list-comma-newline-after\n// Shim for \"new\" HTML5 structural elements to display correctly (IE10, older browsers)\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n// stylelint-enable selector-list-comma-newline-after\n\n// Body\n//\n// 1. Remove the margin in all browsers.\n// 2. As a best practice, apply a default `background-color`.\n// 3. Set an explicit initial text-align value so that we can later use the\n// the `inherit` value on things like `` elements.\n\nbody {\n margin: 0; // 1\n font-family: $font-family-base;\n font-size: $font-size-base;\n font-weight: $font-weight-base;\n line-height: $line-height-base;\n color: $body-color;\n text-align: left; // 3\n background-color: $body-bg; // 2\n}\n\n// Suppress the focus outline on elements that cannot be accessed via keyboard.\n// This prevents an unwanted focus outline from appearing around elements that\n// might still respond to pointer events.\n//\n// Credit: https://github.com/suitcss/base\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\n\n// Content grouping\n//\n// 1. Add the correct box sizing in Firefox.\n// 2. Show the overflow in Edge and IE.\n\nhr {\n box-sizing: content-box; // 1\n height: 0; // 1\n overflow: visible; // 2\n}\n\n\n//\n// Typography\n//\n\n// Remove top margins from headings\n//\n// By default, `

`-`

` all receive top and bottom margins. We nuke the top\n// margin for easier control within type scales as it avoids margin collapsing.\n// stylelint-disable selector-list-comma-newline-after\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: $headings-margin-bottom;\n}\n// stylelint-enable selector-list-comma-newline-after\n\n// Reset margins on paragraphs\n//\n// Similarly, the top margin on `

`s get reset. However, we also reset the\n// bottom margin to use `rem` units instead of `em`.\np {\n margin-top: 0;\n margin-bottom: $paragraph-margin-bottom;\n}\n\n// Abbreviations\n//\n// 1. Remove the bottom border in Firefox 39-.\n// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n// 3. Add explicit cursor to indicate changed behavior.\n// 4. Duplicate behavior to the data-* attribute for our tooltip plugin\n\nabbr[title],\nabbr[data-original-title] { // 4\n text-decoration: underline; // 2\n text-decoration: underline dotted; // 2\n cursor: help; // 3\n border-bottom: 0; // 1\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: $dt-font-weight;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0; // Undo browser default\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic; // Add the correct font style in Android 4.3-\n}\n\n// stylelint-disable font-weight-notation\nb,\nstrong {\n font-weight: bolder; // Add the correct font weight in Chrome, Edge, and Safari\n}\n// stylelint-enable font-weight-notation\n\nsmall {\n font-size: 80%; // Add the correct font size in all browsers\n}\n\n//\n// Prevent `sub` and `sup` elements from affecting the line height in\n// all browsers.\n//\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub { bottom: -.25em; }\nsup { top: -.5em; }\n\n\n//\n// Links\n//\n\na {\n color: $link-color;\n text-decoration: $link-decoration;\n background-color: transparent; // Remove the gray background on active links in IE 10.\n -webkit-text-decoration-skip: objects; // Remove gaps in links underline in iOS 8+ and Safari 8+.\n\n @include hover {\n color: $link-hover-color;\n text-decoration: $link-hover-decoration;\n }\n}\n\n// And undo these styles for placeholder links/named anchors (without href)\n// which have not been made explicitly keyboard-focusable (without tabindex).\n// It would be more straightforward to just use a[href] in previous block, but that\n// causes specificity issues in many other styles that are too complex to fix.\n// See https://github.com/twbs/bootstrap/issues/19402\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n\n @include hover-focus {\n color: inherit;\n text-decoration: none;\n }\n\n &:focus {\n outline: 0;\n }\n}\n\n\n//\n// Code\n//\n\n// stylelint-disable font-family-no-duplicate-names\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; // Correct the inheritance and scaling of font size in all browsers.\n font-size: 1em; // Correct the odd `em` font sizing in all browsers.\n}\n// stylelint-enable font-family-no-duplicate-names\n\npre {\n // Remove browser default top margin\n margin-top: 0;\n // Reset browser default of `1em` to use `rem`s\n margin-bottom: 1rem;\n // Don't allow content to break outside\n overflow: auto;\n // We have @viewport set which causes scrollbars to overlap content in IE11 and Edge, so\n // we force a non-overlapping, non-auto-hiding scrollbar to counteract.\n -ms-overflow-style: scrollbar;\n}\n\n\n//\n// Figures\n//\n\nfigure {\n // Apply a consistent margin strategy (matches our type styles).\n margin: 0 0 1rem;\n}\n\n\n//\n// Images and content\n//\n\nimg {\n vertical-align: middle;\n border-style: none; // Remove the border on images inside links in IE 10-.\n}\n\nsvg:not(:root) {\n overflow: hidden; // Hide the overflow in IE\n}\n\n\n//\n// Tables\n//\n\ntable {\n border-collapse: collapse; // Prevent double borders\n}\n\ncaption {\n padding-top: $table-cell-padding;\n padding-bottom: $table-cell-padding;\n color: $text-muted;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n // Matches default `` alignment by inheriting from the ``, or the\n // closest parent with a set `text-align`.\n text-align: inherit;\n}\n\n\n//\n// Forms\n//\n\nlabel {\n // Allow labels to use `margin` for spacing.\n display: inline-block;\n margin-bottom: .5rem;\n}\n\n// Remove the default `border-radius` that macOS Chrome adds.\n//\n// Details at https://github.com/twbs/bootstrap/issues/24093\nbutton {\n border-radius: 0;\n}\n\n// Work around a Firefox/IE bug where the transparent `button` background\n// results in a loss of the default `button` focus styles.\n//\n// Credit: https://github.com/suitcss/base/\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0; // Remove the margin in Firefox and Safari\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible; // Show the overflow in Edge\n}\n\nbutton,\nselect {\n text-transform: none; // Remove the inheritance of text transform in Firefox\n}\n\n// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`\n// controls in Android 4.\n// 2. Correct the inability to style clickable types in iOS and Safari.\nbutton,\nhtml [type=\"button\"], // 1\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button; // 2\n}\n\n// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box; // 1. Add the correct box sizing in IE 10-\n padding: 0; // 2. Remove the padding in IE 10-\n}\n\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n // Remove the default appearance of temporal inputs to avoid a Mobile Safari\n // bug where setting a custom line-height prevents text from being vertically\n // centered within the input.\n // See https://bugs.webkit.org/show_bug.cgi?id=139848\n // and https://github.com/twbs/bootstrap/issues/11266\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto; // Remove the default vertical scrollbar in IE.\n // Textareas should really only resize vertically so they don't break their (horizontal) containers.\n resize: vertical;\n}\n\nfieldset {\n // Browsers set a default `min-width: min-content;` on fieldsets,\n // unlike e.g. `

`s, which have `min-width: 0;` by default.\n // So we reset that to ensure fieldsets behave more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359\n // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements\n min-width: 0;\n // Reset the default outline behavior of fieldsets so they don't affect page layout.\n padding: 0;\n margin: 0;\n border: 0;\n}\n\n// 1. Correct the text wrapping in Edge and IE.\n// 2. Correct the color inheritance from `fieldset` elements in IE.\nlegend {\n display: block;\n width: 100%;\n max-width: 100%; // 1\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit; // 2\n white-space: normal; // 1\n}\n\nprogress {\n vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.\n}\n\n// Correct the cursor style of increment and decrement buttons in Chrome.\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n // This overrides the extra rounded corners on search inputs in iOS so that our\n // `.form-control` class can properly style them. Note that this cannot simply\n // be added to `.form-control` as it's not specific enough. For details, see\n // https://github.com/twbs/bootstrap/issues/11586.\n outline-offset: -2px; // 2. Correct the outline style in Safari.\n -webkit-appearance: none;\n}\n\n//\n// Remove the inner padding and cancel buttons in Chrome and Safari on macOS.\n//\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// 1. Correct the inability to style clickable types in iOS and Safari.\n// 2. Change font properties to `inherit` in Safari.\n//\n\n::-webkit-file-upload-button {\n font: inherit; // 2\n -webkit-appearance: button; // 1\n}\n\n//\n// Correct element displays\n//\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item; // Add the correct display in all browsers\n cursor: pointer;\n}\n\ntemplate {\n display: none; // Add the correct display in IE\n}\n\n// Always hide an element with the `hidden` HTML attribute (from PureCSS).\n// Needed for proper display in IE 10-.\n[hidden] {\n display: none !important;\n}\n","/*!\n * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n width: device-width;\n}\n\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n -webkit-text-decoration: underline dotted;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n -webkit-text-decoration-skip: objects;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: .5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\nhtml [type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n/*# sourceMappingURL=bootstrap-reboot.css.map */","/*!\n * Bootstrap Reboot v4.0.0 (https://getbootstrap.com)\n * Copyright 2011-2018 The Bootstrap Authors\n * Copyright 2011-2018 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml {\n font-family: sans-serif;\n line-height: 1.15;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n -ms-overflow-style: scrollbar;\n -webkit-tap-highlight-color: transparent;\n}\n\n@-ms-viewport {\n width: device-width;\n}\n\narticle, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {\n display: block;\n}\n\nbody {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";\n font-size: 1rem;\n font-weight: 400;\n line-height: 1.5;\n color: #212529;\n text-align: left;\n background-color: #fff;\n}\n\n[tabindex=\"-1\"]:focus {\n outline: 0 !important;\n}\n\nhr {\n box-sizing: content-box;\n height: 0;\n overflow: visible;\n}\n\nh1, h2, h3, h4, h5, h6 {\n margin-top: 0;\n margin-bottom: 0.5rem;\n}\n\np {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nabbr[title],\nabbr[data-original-title] {\n text-decoration: underline;\n text-decoration: underline dotted;\n cursor: help;\n border-bottom: 0;\n}\n\naddress {\n margin-bottom: 1rem;\n font-style: normal;\n line-height: inherit;\n}\n\nol,\nul,\ndl {\n margin-top: 0;\n margin-bottom: 1rem;\n}\n\nol ol,\nul ul,\nol ul,\nul ol {\n margin-bottom: 0;\n}\n\ndt {\n font-weight: 700;\n}\n\ndd {\n margin-bottom: .5rem;\n margin-left: 0;\n}\n\nblockquote {\n margin: 0 0 1rem;\n}\n\ndfn {\n font-style: italic;\n}\n\nb,\nstrong {\n font-weight: bolder;\n}\n\nsmall {\n font-size: 80%;\n}\n\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -.25em;\n}\n\nsup {\n top: -.5em;\n}\n\na {\n color: #007bff;\n text-decoration: none;\n background-color: transparent;\n -webkit-text-decoration-skip: objects;\n}\n\na:hover {\n color: #0056b3;\n text-decoration: underline;\n}\n\na:not([href]):not([tabindex]) {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):hover, a:not([href]):not([tabindex]):focus {\n color: inherit;\n text-decoration: none;\n}\n\na:not([href]):not([tabindex]):focus {\n outline: 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\npre {\n margin-top: 0;\n margin-bottom: 1rem;\n overflow: auto;\n -ms-overflow-style: scrollbar;\n}\n\nfigure {\n margin: 0 0 1rem;\n}\n\nimg {\n vertical-align: middle;\n border-style: none;\n}\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\ntable {\n border-collapse: collapse;\n}\n\ncaption {\n padding-top: 0.75rem;\n padding-bottom: 0.75rem;\n color: #6c757d;\n text-align: left;\n caption-side: bottom;\n}\n\nth {\n text-align: inherit;\n}\n\nlabel {\n display: inline-block;\n margin-bottom: .5rem;\n}\n\nbutton {\n border-radius: 0;\n}\n\nbutton:focus {\n outline: 1px dotted;\n outline: 5px auto -webkit-focus-ring-color;\n}\n\ninput,\nbutton,\nselect,\noptgroup,\ntextarea {\n margin: 0;\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\nbutton,\ninput {\n overflow: visible;\n}\n\nbutton,\nselect {\n text-transform: none;\n}\n\nbutton,\nhtml [type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n padding: 0;\n border-style: none;\n}\n\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n box-sizing: border-box;\n padding: 0;\n}\n\ninput[type=\"date\"],\ninput[type=\"time\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"] {\n -webkit-appearance: listbox;\n}\n\ntextarea {\n overflow: auto;\n resize: vertical;\n}\n\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n max-width: 100%;\n padding: 0;\n margin-bottom: .5rem;\n font-size: 1.5rem;\n line-height: inherit;\n color: inherit;\n white-space: normal;\n}\n\nprogress {\n vertical-align: baseline;\n}\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n[type=\"search\"] {\n outline-offset: -2px;\n -webkit-appearance: none;\n}\n\n[type=\"search\"]::-webkit-search-cancel-button,\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n::-webkit-file-upload-button {\n font: inherit;\n -webkit-appearance: button;\n}\n\noutput {\n display: inline-block;\n}\n\nsummary {\n display: list-item;\n cursor: pointer;\n}\n\ntemplate {\n display: none;\n}\n\n[hidden] {\n display: none !important;\n}\n\n/*# sourceMappingURL=bootstrap-reboot.css.map */","// stylelint-disable indentation\n\n// Hover mixin and `$enable-hover-media-query` are deprecated.\n//\n// Origally added during our alphas and maintained during betas, this mixin was\n// designed to prevent `:hover` stickiness on iOS—an issue where hover styles\n// would persist after initial touch.\n//\n// For backward compatibility, we've kept these mixins and updated them to\n// always return their regular psuedo-classes instead of a shimmed media query.\n//\n// Issue: https://github.com/twbs/bootstrap/issues/25195\n\n@mixin hover {\n &:hover { @content; }\n}\n\n@mixin hover-focus {\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin plain-hover-focus {\n &,\n &:hover,\n &:focus {\n @content;\n }\n}\n\n@mixin hover-focus-active {\n &:hover,\n &:focus,\n &:active {\n @content;\n }\n}\n"]} --------------------------------------------------------------------------------