├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CONTRIBUTING.md ├── GitCommitsAnalysis.py ├── GitCommitsAnalysis ├── Analysers │ ├── CodeAnalyser.cs │ ├── CyclomaticComplexityCounter.cs │ ├── LinesOfCodeCalculator.cs │ └── MethodCounter.cs ├── GitCommitsAnalysis.cs ├── GitCommitsAnalysis.csproj ├── GitCommitsAnalysis.sln ├── GlobalSuppressions.cs ├── Interfaces │ ├── IReport.cs │ └── ISystemIO.cs ├── Model │ ├── Analysis.cs │ ├── FileStat.cs │ ├── FolderStat.cs │ ├── ScatterPoint.cs │ └── UserFilename.cs ├── Options.cs ├── Program.cs ├── Reporting │ ├── BaseReport.cs │ ├── ExcelReport.cs │ ├── HTMLReport.cs │ ├── JsonReport.cs │ ├── MarkdownReport.cs │ └── TextFileReport.cs └── SystemIO.cs ├── LICENSE ├── README.md └── screenshots ├── HtmlReport1.png ├── HtmlReport2.png ├── HtmlReport3.png └── HtmlReport4.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Parameters used 16 | 2. Is there a GitHub repository that can be used to reproduce the issue 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | 21 | **Screenshots** 22 | If applicable, add screenshots to help explain your problem. 23 | 24 | **Desktop (please complete the following information):** 25 | - OS: [e.g. iOS] 26 | - Browser [e.g. chrome, safari] 27 | - Version [e.g. 22] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # vscode 123 | .vscode 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | 259 | # Microsoft Fakes 260 | FakesAssemblies/ 261 | 262 | # GhostDoc plugin setting file 263 | *.GhostDoc.xml 264 | 265 | # Node.js Tools for Visual Studio 266 | .ntvs_analysis.dat 267 | node_modules/ 268 | 269 | # Visual Studio 6 build log 270 | *.plg 271 | 272 | # Visual Studio 6 workspace options file 273 | *.opt 274 | 275 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 276 | *.vbw 277 | 278 | # Visual Studio LightSwitch build output 279 | **/*.HTMLClient/GeneratedArtifacts 280 | **/*.DesktopClient/GeneratedArtifacts 281 | **/*.DesktopClient/ModelManifest.xml 282 | **/*.Server/GeneratedArtifacts 283 | **/*.Server/ModelManifest.xml 284 | _Pvt_Extensions 285 | 286 | # Paket dependency manager 287 | .paket/paket.exe 288 | paket-files/ 289 | 290 | # FAKE - F# Make 291 | .fake/ 292 | 293 | # JetBrains Rider 294 | .idea/ 295 | *.sln.iml 296 | 297 | # CodeRush 298 | .cr/ 299 | 300 | # Python Tools for Visual Studio (PTVS) 301 | __pycache__/ 302 | *.pyc 303 | 304 | # Cake - Uncomment if you are using it 305 | # tools/** 306 | # !tools/packages.config 307 | 308 | # Tabs Studio 309 | *.tss 310 | 311 | # Telerik's JustMock configuration file 312 | *.jmconfig 313 | 314 | # BizTalk build output 315 | *.btp.cs 316 | *.btm.cs 317 | *.odx.cs 318 | *.xsd.cs 319 | 320 | # OpenCover UI analysis results 321 | OpenCover/ 322 | 323 | # Azure Stream Analytics local run output 324 | ASALocalRun/ 325 | 326 | # MSBuild Binary and Structured Log 327 | *.binlog 328 | 329 | # NVidia Nsight GPU debugger configuration file 330 | *.nvuser 331 | 332 | # MFractors (Xamarin productivity tool) working folder 333 | .mfractor/ 334 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Any ideas, bug reports or help building this tool, is greatly appreciated. 4 | 5 | ## Bugs 6 | 7 | If you find a bug feel free to post a issue describing the steps to reproduce the issue and what the expected behavior should have been. The GitHub issue template should guide you. 8 | 9 | ## ideas 10 | 11 | This tool can be extended in a lot of ways, but this is a hobby project for me and I have only limited time to work on it, so if you have a great idea, create a feature request in the issues section and i will have a look at it. 12 | 13 | To increase to probability of the feature getting implemented you could help by creating a pull request with an implementation of the feature and I will give it a serious look. 14 | -------------------------------------------------------------------------------- /GitCommitsAnalysis.py: -------------------------------------------------------------------------------- 1 | import os 2 | import operator 3 | from git import Repo 4 | 5 | # Script inspired by this youtube video: https://www.youtube.com/watch?v=a74UkJxKWVM&t=2450s 6 | 7 | # GitPython code from example at https://www.fullstackpython.com/blog/first-steps-gitpython.html 8 | 9 | class FileStats: 10 | def __init__(self, filename: str, username: str, numberOfChanges: int): 11 | self.Filename = filename 12 | self.Username = username 13 | self.NumberOfChanges = numberOfChanges 14 | 15 | 16 | fileChanges = {} 17 | fileChangesByUser = {} 18 | 19 | 20 | def print_commit(commit): 21 | print('----') 22 | print(str(commit.hexsha)) 23 | print("\"{}\" by {} ({})".format(commit.summary, 24 | commit.author.name, 25 | commit.author.email)) 26 | print(str(commit.authored_datetime)) 27 | print(str("count: {} and size: {}".format(commit.count(), 28 | commit.size))) 29 | print(commit.tree) 30 | 31 | 32 | def calculateNumberOfChanges(commit): 33 | for filename in commit.stats.files: 34 | # print(str("- {}".format(file))) 35 | if filename in fileChanges: 36 | fileChanges[filename].NumberOfChanges += 1 37 | else: 38 | fileChanges[filename] = FileStats(filename, "", 1) 39 | authorname = commit.author.name 40 | key = str(filename + ', ' + authorname) 41 | if key in fileChangesByUser: 42 | fileChangesByUser[key].NumberOfChanges += 1 43 | else: 44 | fileChangesByUser[key] = FileStats(filename, authorname, 1) 45 | 46 | def print_repository(repo): 47 | print('Repo description: {}'.format(repo.description)) 48 | print('Repo active branch is {}'.format(repo.active_branch)) 49 | for remote in repo.remotes: 50 | print('Remote named "{}" with URL "{}"'.format(remote, remote.url)) 51 | print('Last commit for repo is {}.'.format(str(repo.head.commit.hexsha))) 52 | 53 | 54 | if __name__ == "__main__": 55 | repo_path = "." # os.getenv('GIT_REPO_PATH') 56 | # Repo object used to programmatically interact with Git repositories 57 | repo = Repo(repo_path) 58 | # check that the repository loaded correctly 59 | if not repo.bare: 60 | print('Repo at {} successfully loaded.'.format(repo_path)) 61 | print_repository(repo) 62 | # create list of commits then print some of them to stdout 63 | commits = list(repo.iter_commits('master')) 64 | numberOfCommits = len(commits) 65 | print("Analysing commits: {}".format(numberOfCommits)) 66 | for commit in commits: 67 | # print_commit(commit) 68 | calculateNumberOfChanges(commit) 69 | pass 70 | print("----------------------------------------") 71 | for filename in (sorted(fileChanges, key=lambda filename: fileChanges[filename].NumberOfChanges, reverse=True)): 72 | print( 73 | "- {}: {}".format(fileChanges[filename].Filename, fileChanges[filename].NumberOfChanges)) 74 | for filenameAuthor in (sorted(fileChangesByUser, key=lambda filenameAuthor: fileChangesByUser[filenameAuthor].NumberOfChanges, reverse=True)): 75 | if(fileChangesByUser[filenameAuthor].NumberOfChanges > 2): 76 | print("- {}, {}: {}".format(fileChangesByUser[filenameAuthor].Filename, 77 | fileChangesByUser[filenameAuthor].Username, 78 | fileChangesByUser[filenameAuthor].NumberOfChanges)) 79 | filenameAuthor 80 | 81 | else: 82 | print('Could not load repository at {} :('.format(repo_path)) 83 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Analysers/CodeAnalyser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace GitCommitsAnalysis.Analysers 8 | { 9 | public static class CodeAnalyser 10 | { 11 | public static SyntaxTree GetSyntaxTree(string fileContents) 12 | { 13 | var tree = CSharpSyntaxTree.ParseText(fileContents); 14 | return tree; 15 | } 16 | public static IEnumerable GetMethodDeclarationSyntaxe(SyntaxTree tree) 17 | { 18 | var syntaxNode = tree 19 | .GetRoot() 20 | .DescendantNodes() 21 | .OfType(); 22 | return syntaxNode; 23 | } 24 | 25 | public static SemanticModel GetModel(SyntaxTree tree) 26 | { 27 | var compilation = CSharpCompilation.Create( 28 | "x", 29 | syntaxTrees: new[] { tree }, 30 | references: 31 | new MetadataReference[] 32 | { 33 | MetadataReference.CreateFromFile(typeof (object).Assembly.Location) 34 | }); 35 | 36 | var model = compilation.GetSemanticModel(tree, true); 37 | return model; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Analysers/CyclomaticComplexityCounter.cs: -------------------------------------------------------------------------------- 1 | /** 2 | * This class was originally created by: 3 | * https://github.com/jjrdk/ArchiMetrics 4 | */ 5 | namespace GitCommitsAnalysis.Analysers 6 | { 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using Microsoft.CodeAnalysis; 10 | using Microsoft.CodeAnalysis.CSharp; 11 | using Microsoft.CodeAnalysis.CSharp.Syntax; 12 | 13 | public class CyclomaticComplexityCounter 14 | { 15 | public int Calculate(IEnumerable syntaxNode, SyntaxTree syntaxTree) 16 | { 17 | var model = CodeAnalyser.GetModel(syntaxTree); 18 | int result = 1; 19 | foreach(var node in syntaxNode) 20 | { 21 | result += Calculate(node, model); 22 | } 23 | return result; 24 | } 25 | 26 | public int Calculate(SyntaxNode node, SemanticModel semanticModel) 27 | { 28 | var analyzer = new InnerComplexityAnalyzer(semanticModel); 29 | var result = analyzer.Calculate(node); 30 | 31 | return result; 32 | } 33 | 34 | private class InnerComplexityAnalyzer : CSharpSyntaxWalker 35 | { 36 | private static readonly SyntaxKind[] Contributors = new[] 37 | { 38 | SyntaxKind.CaseSwitchLabel, 39 | SyntaxKind.CoalesceExpression, 40 | SyntaxKind.ConditionalExpression, 41 | SyntaxKind.LogicalAndExpression, 42 | SyntaxKind.LogicalOrExpression, 43 | SyntaxKind.LogicalNotExpression 44 | }; 45 | 46 | // private static readonly string[] LazyTypes = new[] { "System.Threading.Tasks.Task" }; 47 | private readonly SemanticModel _semanticModel; 48 | private int _counter; 49 | 50 | public InnerComplexityAnalyzer(SemanticModel semanticModel) 51 | : base(SyntaxWalkerDepth.Node) 52 | { 53 | _semanticModel = semanticModel; 54 | _counter = 1; 55 | } 56 | 57 | public int Calculate(SyntaxNode syntax) 58 | { 59 | if (syntax != null) 60 | { 61 | Visit(syntax); 62 | } 63 | 64 | return _counter; 65 | } 66 | 67 | public override void Visit(SyntaxNode node) 68 | { 69 | base.Visit(node); 70 | if (Contributors.Contains(node.Kind())) 71 | { 72 | _counter++; 73 | } 74 | } 75 | 76 | public override void VisitWhileStatement(WhileStatementSyntax node) 77 | { 78 | base.VisitWhileStatement(node); 79 | _counter++; 80 | } 81 | 82 | public override void VisitForStatement(ForStatementSyntax node) 83 | { 84 | base.VisitForStatement(node); 85 | _counter++; 86 | } 87 | 88 | public override void VisitForEachStatement(ForEachStatementSyntax node) 89 | { 90 | base.VisitForEachStatement(node); 91 | _counter++; 92 | } 93 | 94 | //// TODO: Calculate for tasks 95 | ////public override void VisitInvocationExpression(InvocationExpressionSyntax node) 96 | ////{ 97 | //// if (_semanticModel != null) 98 | //// { 99 | //// var symbol = _semanticModel.GetSymbolInfo(node).Symbol; 100 | //// if (symbol != null) 101 | //// { 102 | //// switch (symbol.Kind) 103 | //// { 104 | //// case SymbolKind.Method: 105 | //// var returnType = ((IMethodSymbol)symbol).ReturnType; 106 | //// break; 107 | //// } 108 | //// } 109 | //// } 110 | //// base.VisitInvocationExpression(node); 111 | ////} 112 | 113 | //// base.VisitInvocationExpression(node); 114 | ////} 115 | public override void VisitArgument(ArgumentSyntax node) 116 | { 117 | switch (node.Expression.Kind()) 118 | { 119 | case SyntaxKind.ParenthesizedLambdaExpression: 120 | { 121 | var lambda = (ParenthesizedLambdaExpressionSyntax)node.Expression; 122 | Visit(lambda.Body); 123 | } 124 | 125 | break; 126 | case SyntaxKind.SimpleLambdaExpression: 127 | { 128 | var lambda = (SimpleLambdaExpressionSyntax)node.Expression; 129 | Visit(lambda.Body); 130 | } 131 | 132 | break; 133 | } 134 | 135 | base.VisitArgument(node); 136 | } 137 | 138 | public override void VisitDefaultExpression(DefaultExpressionSyntax node) 139 | { 140 | base.VisitDefaultExpression(node); 141 | _counter++; 142 | } 143 | 144 | public override void VisitContinueStatement(ContinueStatementSyntax node) 145 | { 146 | base.VisitContinueStatement(node); 147 | _counter++; 148 | } 149 | 150 | public override void VisitGotoStatement(GotoStatementSyntax node) 151 | { 152 | base.VisitGotoStatement(node); 153 | _counter++; 154 | } 155 | 156 | public override void VisitIfStatement(IfStatementSyntax node) 157 | { 158 | base.VisitIfStatement(node); 159 | _counter++; 160 | } 161 | 162 | public override void VisitCatchClause(CatchClauseSyntax node) 163 | { 164 | base.VisitCatchClause(node); 165 | _counter++; 166 | } 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /GitCommitsAnalysis/Analysers/LinesOfCodeCalculator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.IO; 4 | 5 | namespace GitCommitsAnalysis.Analysers 6 | { 7 | 8 | public class LinesOfCodeCalculator 9 | { 10 | public int Calculate(string fileContents) 11 | { 12 | var lines = fileContents.Split(new string[] { "\r\n", "\n\r", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); 13 | return lines.Where(l => !l.Trim().StartsWith("using")).ToList().Count(); 14 | } 15 | 16 | } 17 | } -------------------------------------------------------------------------------- /GitCommitsAnalysis/Analysers/MethodCounter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Zu.TypeScript; 6 | 7 | namespace GitCommitsAnalysis.Analysers 8 | { 9 | 10 | public static class MethodCounter 11 | { 12 | public static int Calculate(IEnumerable syntaxNode) 13 | { 14 | int result = syntaxNode.Count(); 15 | return result; 16 | } 17 | 18 | public static int Calculate(TypeScriptAST typeScriptAst, string fileContents) 19 | { 20 | typeScriptAst.MakeAST(fileContents); 21 | var methods = typeScriptAst.OfKind(Zu.TypeScript.TsTypes.SyntaxKind.MethodDeclaration); 22 | var functions = typeScriptAst.OfKind(Zu.TypeScript.TsTypes.SyntaxKind.FunctionDeclaration); 23 | return functions.Count() + methods.Count(); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /GitCommitsAnalysis/GitCommitsAnalysis.cs: -------------------------------------------------------------------------------- 1 | using LibGit2Sharp; 2 | using GitCommitsAnalysis.Analysers; 3 | using GitCommitsAnalysis.Interfaces; 4 | using GitCommitsAnalysis.Model; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using Zu.TypeScript; 10 | 11 | namespace GitCommitsAnalysis 12 | { 13 | public class GitCommitsAnalysis 14 | { 15 | private ISystemIO fileHandling; 16 | private IEnumerable reports; 17 | private Options options; 18 | 19 | public GitCommitsAnalysis(ISystemIO fileHandling, IEnumerable reports, Options options) 20 | { 21 | this.fileHandling = fileHandling; 22 | this.reports = reports; 23 | this.options = options; 24 | } 25 | 26 | public void PerformAnalysis(string rootFolder) 27 | { 28 | Console.WriteLine("Analyzing commits..."); 29 | using (var repo = new Repository(rootFolder)) 30 | { 31 | var analysis = new Analysis(); 32 | var renamedFiles = new Dictionary(); 33 | var cyclomaticComplexityCounter = new CyclomaticComplexityCounter(); 34 | var linesOfCodeCalculator = new LinesOfCodeCalculator(); 35 | var typeScriptAst = new TypeScriptAST(); 36 | foreach(var tag in repo.Tags) 37 | { 38 | var commit = repo.Lookup(tag.Target.Sha); 39 | var commitDate = commit.Author.When.UtcDateTime.Date; 40 | analysis.Tags.Add(commitDate, tag.FriendlyName); 41 | } 42 | foreach(var branch in repo.Branches.Where(br => br.IsRemote)) 43 | { 44 | analysis.Branches.Add(branch.FriendlyName); 45 | } 46 | foreach (var commit in repo.Commits) 47 | { 48 | var username = commit.Author.Name; 49 | var commitDate = commit.Author.When.UtcDateTime.Date; 50 | UpdateAnalysisCommitDates(analysis, commitDate); 51 | IncDictionaryValue(analysis.CommitsEachDay, commitDate); 52 | foreach (var parent in commit.Parents) 53 | { 54 | var patch = repo.Diff.Compare(parent.Tree, commit.Tree); 55 | IncDictionaryValue(analysis.LinesOfCodeAddedEachDay, commitDate, patch.LinesAdded); 56 | IncDictionaryValue(analysis.LinesOfCodeDeletedEachDay, commitDate, patch.LinesDeleted); 57 | foreach (TreeEntryChanges change in repo.Diff.Compare(parent.Tree, commit.Tree)) 58 | { 59 | int cyclomaticComplexity = 0; 60 | int methodCount = 0; 61 | var fullPath = Path.Combine(rootFolder, change.Path); 62 | if (change.Path != change.OldPath) 63 | { 64 | if (analysis.FileCommits.ContainsKey(change.OldPath)) 65 | { 66 | analysis.FileCommits[change.Path] = analysis.FileCommits[change.OldPath]; 67 | analysis.FileCommits.Remove(change.OldPath); 68 | } 69 | if (!renamedFiles.ContainsKey(change.OldPath)) 70 | { 71 | renamedFiles.Add(change.OldPath, change.Path); 72 | } 73 | } 74 | string filename = renamedFiles.ContainsKey(change.OldPath) ? renamedFiles[change.OldPath] : change.Path; 75 | var fileType = Path.GetExtension(filename); 76 | if (IgnoreFiletype(fileType)) 77 | { 78 | break; 79 | } 80 | 81 | if (analysis.FileCommits.ContainsKey(filename)) 82 | { 83 | analysis.FileCommits[filename].CommitCount++; 84 | } 85 | else 86 | { 87 | int linesOfCode = 0; 88 | var fileExists = fileHandling.FileExists(fullPath); 89 | if (fileExists) 90 | { 91 | var fileContents = fileHandling.ReadFileContent(fullPath); 92 | linesOfCode = linesOfCodeCalculator.Calculate(fileContents); 93 | if (change.Path.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) 94 | { 95 | var syntaxTree = CodeAnalyser.GetSyntaxTree(fileContents); 96 | var methodDeclarationNode = CodeAnalyser.GetMethodDeclarationSyntaxe(syntaxTree); 97 | cyclomaticComplexity = cyclomaticComplexityCounter.Calculate(methodDeclarationNode, syntaxTree); 98 | methodCount = MethodCounter.Calculate(methodDeclarationNode); 99 | } 100 | else if (change.Path.EndsWith(".ts", StringComparison.OrdinalIgnoreCase)) 101 | { 102 | methodCount = MethodCounter.Calculate(typeScriptAst, fileContents); 103 | } 104 | analysis.LinesOfCodeanalyzed += linesOfCode; 105 | } 106 | analysis.FileCommits[filename] = new FileStat { Filename = filename, CyclomaticComplexity = cyclomaticComplexity, LinesOfCode = linesOfCode, MethodCount = methodCount, FileExists = fileExists }; 107 | IncDictionaryValue(analysis.FileTypes, fileType); 108 | } 109 | analysis.FileCommits[filename].CommitDates.Add(commitDate); 110 | if(analysis.FileCommits[filename].LatestCommit < commitDate) 111 | { 112 | analysis.FileCommits[filename].LatestCommit = commitDate; 113 | } 114 | IncDictionaryValue(analysis.CodeAge, analysis.FileCommits[filename].CodeAge); 115 | 116 | var usernameFilename = UsernameFilename.GetDictKey(filename, username); 117 | if (analysis.UserfileCommits.ContainsKey(usernameFilename)) { analysis.UserfileCommits[usernameFilename].CommitCount++; } else { analysis.UserfileCommits[usernameFilename] = new FileStat { Filename = filename, Username = username }; } 118 | analysis.UserfileCommits[usernameFilename].CommitDates.Add(commitDate); 119 | } 120 | } 121 | } 122 | var folderStats = new Dictionary(); 123 | foreach(var fileChange in analysis.FileCommits) 124 | { 125 | int commitCount = fileChange.Value.CommitCount; 126 | var folders = fileChange.Key.Split("/"); 127 | string root = folders[0]; 128 | if(fileChange.Key.IndexOf("/") == -1){ 129 | root = "."; 130 | } 131 | IncFolderCommitValue(analysis.FolderCommits, root, commitCount); 132 | analysis.FolderCommits[root].IsRoot = true; 133 | string currentFolder = root; 134 | var children = analysis.FolderCommits[currentFolder].Children; 135 | for(int i = 1; i < folders.Length; i++) 136 | { 137 | currentFolder = folders[i]; 138 | IncFolderCommitValue(children, currentFolder, commitCount); 139 | children = children[currentFolder].Children; 140 | } 141 | var codeAge = fileChange.Value.CommitDates.OrderByDescending(cd => cd).First(); 142 | } 143 | var o = analysis.FolderCommits.OrderBy(p => p.Key); 144 | analysis.AnalysisTime = (DateTime.UtcNow.Ticks - analysis.CreatedDate.Ticks) / 10000; // Analysis time in milliseconds 145 | foreach (var report in reports) 146 | { 147 | report.Generate(analysis); 148 | } 149 | } 150 | } 151 | 152 | private static void IncDictionaryValue(Dictionary dictionary, T key, int increment = 1) 153 | { 154 | if (dictionary.ContainsKey(key)) 155 | { 156 | dictionary[key] += increment; 157 | } 158 | else 159 | { 160 | dictionary[key] = increment; 161 | } 162 | } 163 | 164 | private static void IncFolderCommitValue(Dictionary dictionary, string key, int increment) 165 | { 166 | if (string.IsNullOrEmpty(key)) 167 | { 168 | key = "."; 169 | } 170 | if (dictionary.ContainsKey(key)) 171 | { 172 | dictionary[key].FileChanges += increment; 173 | } 174 | else 175 | { 176 | dictionary[key] = new FolderStat(key, increment); 177 | } 178 | } 179 | 180 | private static void UpdateAnalysisCommitDates(Analysis analysis, DateTime commitDate) 181 | { 182 | if(commitDate < analysis.FirstCommitDate) 183 | { 184 | analysis.FirstCommitDate = commitDate; 185 | } 186 | else if (commitDate > analysis.LatestCommitDate) 187 | { 188 | analysis.LatestCommitDate = commitDate; 189 | } 190 | } 191 | 192 | private bool IgnoreFiletype(string fileExtension) 193 | { 194 | if (!string.IsNullOrEmpty(fileExtension)) 195 | { 196 | fileExtension = fileExtension.Substring(1); 197 | if (options.IgnoredFiletypes != null && options.IgnoredFiletypes.Any() && options.IgnoredFiletypes.Contains(fileExtension)) 198 | { 199 | return true; 200 | } 201 | } 202 | return false; 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/GitCommitsAnalysis.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/GitCommitsAnalysis.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29326.143 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GitCommitsAnalysis", "GitCommitsAnalysis.csproj", "{8A03FD80-1F44-42F8-8322-DE676B16C90C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {8A03FD80-1F44-42F8-8322-DE676B16C90C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {8A03FD80-1F44-42F8-8322-DE676B16C90C}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {8A03FD80-1F44-42F8-8322-DE676B16C90C}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {8A03FD80-1F44-42F8-8322-DE676B16C90C}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {79EA7C79-9E45-4E5B-8C01-3B3A0E2AD5DC} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "", Scope = "member", Target = "~M:GitCommitsAnalysis.Reporting.HTMLReport.Generate(System.Collections.Generic.Dictionary{System.String,GitCommitsAnalysis.Model.FileStat},System.Collections.Generic.Dictionary{System.String,GitCommitsAnalysis.Model.FileStat},System.Collections.Generic.Dictionary{System.String,GitCommitsAnalysis.Model.FileStat})")] 7 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Interfaces/IReport.cs: -------------------------------------------------------------------------------- 1 | using GitCommitsAnalysis.Model; 2 | 3 | namespace GitCommitsAnalysis.Interfaces 4 | { 5 | public interface IReport 6 | { 7 | void Generate(Analysis analysis); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Interfaces/ISystemIO.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace GitCommitsAnalysis.Interfaces 4 | { 5 | public interface ISystemIO 6 | { 7 | string ReadFileContent(string filename); 8 | void WriteAllText(string filename, string contents); 9 | bool FileExists(string filename); 10 | string GetPathWitoutExtension(string filename); 11 | string GetExtension(string filename); 12 | FileInfo FileInfo(string filename); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Model/Analysis.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCommitsAnalysis.Model 5 | { 6 | public class Analysis 7 | { 8 | public DateTime CreatedDate { get; } = DateTime.UtcNow; 9 | public DateTime FirstCommitDate { get; set; } = DateTime.MaxValue; 10 | public DateTime LatestCommitDate { get; set; } = DateTime.MinValue; 11 | public long AnalysisTime { get; set; } 12 | public long LinesOfCodeanalyzed { get; set; } = 0; 13 | public Dictionary CommitsEachDay { get; } = new Dictionary(); 14 | public Dictionary LinesOfCodeAddedEachDay { get; } = new Dictionary(); 15 | public Dictionary LinesOfCodeDeletedEachDay { get; } = new Dictionary(); 16 | public Dictionary Tags { get; } = new Dictionary(); 17 | public List Branches { get; } = new List(); 18 | public Dictionary FileCommits { get; } = new Dictionary(); 19 | public Dictionary FolderCommits { get; } = new Dictionary(); 20 | public Dictionary UserfileCommits { get; } = new Dictionary(); 21 | public Dictionary FileTypes { get; } = new Dictionary(); 22 | public Dictionary CodeAge { get; } = new Dictionary(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Model/FileStat.cs: -------------------------------------------------------------------------------- 1 | namespace GitCommitsAnalysis.Model 2 | { 3 | 4 | public class FileStat : UsernameFilename 5 | { 6 | public int CommitCount { get; set; } = 1; 7 | public int MethodCount { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Model/FolderStat.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace GitCommitsAnalysis.Model 4 | { 5 | public class FolderStat 6 | { 7 | public FolderStat(string folderName, int fileChanges) 8 | { 9 | FolderName = folderName; 10 | FileChanges = fileChanges; 11 | } 12 | 13 | public bool IsRoot { get; set; } = false; 14 | public string FolderName { get; set; } 15 | public int FileChanges { get; set; } = 0; 16 | public Dictionary Children { get; } = new Dictionary(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Model/ScatterPoint.cs: -------------------------------------------------------------------------------- 1 | namespace GitCommitsAnalysis.Model 2 | { 3 | public class ScatterPoint 4 | { 5 | public string Date { get; set; } 6 | public int UserId { get; set; } 7 | public string ToolTip { get; set; } 8 | 9 | public override string ToString() 10 | { 11 | return $"[{Date},{UserId},{ToolTip}]"; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Model/UserFilename.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCommitsAnalysis.Model 5 | { 6 | public class UsernameFilename 7 | { 8 | public string Username { get; set; } 9 | public string Filename { get; set; } 10 | public List CommitDates { get; } = new List(); 11 | public int? CyclomaticComplexity { get; set; } = null; 12 | public int? LinesOfCode { get; set; } = null; 13 | public bool FileExists { get; set; } = false; 14 | public DateTime LatestCommit { get; set; } = DateTime.MinValue; 15 | public int CodeAge 16 | { 17 | get 18 | { 19 | var now = DateTime.Now; 20 | int monthsApart = 12 * (now.Year - LatestCommit.Year) + now.Month - LatestCommit.Month; 21 | return Math.Abs(monthsApart); 22 | } 23 | } 24 | 25 | public static string GetDictKey(string filename, string username) 26 | { 27 | return filename + "*" + username; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Options.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CommandLine; 3 | 4 | namespace GitCommitsAnalysis 5 | { 6 | public class Options 7 | { 8 | [Option('r', "rootfolder", Required = true, HelpText = "The root folder of the application source code")] 9 | public string RootFolder { get; set; } 10 | 11 | [Option('o', "outputfolder", Required = true, HelpText = "The output folder where the generated reports will be placed")] 12 | public string OutputFolder { get; set; } 13 | 14 | [Option('a', "reportfilename", Required = false, HelpText = "The filename the report(s) will be given")] 15 | public string ReportFilename { get; set; } 16 | 17 | [Option('f', "outputformat", Required = true, HelpText = "The output format(s) to generate. Multiple formats should be space-separated. Eg. '-f Text Json'. Valid formats: HTML, Markdown, Json, Text, Excel")] 18 | public IEnumerable OutputFormat { get; set; } 19 | 20 | [Option('n', "numberoffilestolist", Default = 50, HelpText = "Specifies the number of flies to include in the list of most changes files. (Ignored when output is Json)")] 21 | public int NumberOfFilesInList { get; set; } 22 | 23 | [Option('t', "title", Default = "GitCommitsAnalysis", HelpText = "The title to appear in the top of the reports")] 24 | public string Title { get; set; } 25 | 26 | [Option('i', "ignoredfiletypes", HelpText = "The file types to ignore when analyzing the Git repository.. Eg. '-i csproj npmrc gitignore'")] 27 | public IEnumerable IgnoredFiletypes { get; set; } 28 | } 29 | 30 | public enum OutputFormat 31 | { 32 | Text, 33 | Markdown, 34 | Json, 35 | HTML, 36 | Excel 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Program.cs: -------------------------------------------------------------------------------- 1 | using GitCommitsAnalysis.Interfaces; 2 | using GitCommitsAnalysis.Reporting; 3 | using System.Collections.Generic; 4 | using CommandLine; 5 | using CommandLine.Text; 6 | using System; 7 | 8 | namespace GitCommitsAnalysis 9 | { 10 | public static class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | using (var parser = new Parser(with => with.HelpWriter = null)) 15 | { 16 | var parserResult = parser.ParseArguments(args); 17 | parserResult.WithParsed(options => 18 | { 19 | var systemIO = new SystemIO(); 20 | var reports = getReportGenerators(options, systemIO); 21 | var GitCommitsAnalysis = new GitCommitsAnalysis(systemIO, reports, options); 22 | 23 | GitCommitsAnalysis.PerformAnalysis(options.RootFolder); 24 | }).WithNotParsed(x => 25 | { 26 | var helpText = HelpText.AutoBuild(parserResult, h => 27 | { 28 | h.AutoHelp = false; //hide --help 29 | h.AutoVersion = false; //hide --version 30 | return HelpText.DefaultParsingErrorsHandler(parserResult, h); 31 | }, e => e); 32 | Console.WriteLine(helpText); 33 | }); 34 | } 35 | } 36 | 37 | private static List getReportGenerators(Options options, ISystemIO systemIO) 38 | { 39 | var reportGenerators = new List(); 40 | foreach (var format in options.OutputFormat) 41 | { 42 | string outputFilename = "GitCommitsAnalysisReport"; 43 | if (!string.IsNullOrEmpty(options.ReportFilename)) 44 | { 45 | outputFilename = systemIO.GetPathWitoutExtension(options.ReportFilename); 46 | } 47 | var filename = $"{options.OutputFolder}\\{outputFilename}"; 48 | if (format == OutputFormat.Text) 49 | { 50 | reportGenerators.Add(new TextFileReport(systemIO, filename, options)); 51 | } 52 | if (format == OutputFormat.Markdown) 53 | { 54 | reportGenerators.Add(new MarkdownReport(systemIO, filename, options)); 55 | } 56 | if (format == OutputFormat.Json) 57 | { 58 | reportGenerators.Add(new JsonReport(systemIO, filename, options)); 59 | } 60 | if (format == OutputFormat.HTML) 61 | { 62 | reportGenerators.Add(new HTMLReport(systemIO, filename, options)); 63 | } 64 | if (format == OutputFormat.Excel) 65 | { 66 | reportGenerators.Add(new ExcelReport(systemIO, filename, options)); 67 | } 68 | } 69 | return reportGenerators; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Reporting/BaseReport.cs: -------------------------------------------------------------------------------- 1 | using GitCommitsAnalysis.Interfaces; 2 | using GitCommitsAnalysis.Model; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace GitCommitsAnalysis.Reporting 7 | { 8 | public class BaseReport 9 | { 10 | protected string ReportFilename { get; set; } 11 | protected string Title { get; set; } 12 | protected int NumberOfFilesToList { get; set; } 13 | protected ISystemIO SystemIO { get; set; } 14 | protected IOrderedEnumerable FileCommitsList { get; set; } 15 | protected IOrderedEnumerable UserfileCommitsList { get; set; } 16 | protected IOrderedEnumerable FolderCommitsList { get; set; } 17 | protected Dictionary UserNameKey { get; } = new Dictionary(); 18 | protected Dictionary FolderCommits { get; set; } 19 | protected BaseReport(ISystemIO systemIO, string reportFilename, Options options) 20 | { 21 | this.SystemIO = systemIO; 22 | this.ReportFilename = reportFilename; 23 | this.Title = options.Title; 24 | this.NumberOfFilesToList = options.NumberOfFilesInList; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Reporting/ExcelReport.cs: -------------------------------------------------------------------------------- 1 | using OfficeOpenXml; 2 | using GitCommitsAnalysis.Interfaces; 3 | using GitCommitsAnalysis.Model; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace GitCommitsAnalysis.Reporting 9 | { 10 | public class ExcelReport : BaseReport, IReport 11 | { 12 | private readonly ISystemIO systemIO; 13 | public ExcelReport(ISystemIO systemIO, string reportFilename, Options options) : base(systemIO, reportFilename, options) 14 | { 15 | this.systemIO = systemIO; 16 | } 17 | 18 | public void Generate(Analysis analysis) 19 | { 20 | Console.WriteLine("Generating Excel report..."); 21 | if (analysis == null) throw new ArgumentException("Parameter analysis is null.", nameof(analysis)); 22 | this.FileCommitsList = analysis.FileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename); 23 | this.UserfileCommitsList = analysis.UserfileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename).ThenBy(fc => fc.Username); 24 | this.FolderCommits = analysis.FolderCommits; 25 | this.FolderCommitsList = analysis.FolderCommits.Values.OrderByDescending(fc => fc.FileChanges); 26 | ExcelPackage.LicenseContext = LicenseContext.NonCommercial; 27 | 28 | using (var excelPackage = new ExcelPackage()) 29 | { 30 | var sheetCommitsForEachSubfolder = excelPackage.Workbook.Worksheets.Add("Commits for each sub-folder"); 31 | AddSectionCommitsForEachMonth(sheetCommitsForEachSubfolder); 32 | 33 | var sheetTopMostChangedFiles = excelPackage.Workbook.Worksheets.Add($"Top {NumberOfFilesToList} most changed files"); 34 | AddSectionCommitsForEachFile(sheetTopMostChangedFiles); 35 | 36 | var sheetStatistics = excelPackage.Workbook.Worksheets.Add("Statistics"); 37 | AddSectionStatistics(sheetStatistics, analysis); 38 | 39 | var sheetCommitsEachDay = excelPackage.Workbook.Worksheets.Add("Commits each day"); 40 | AddSectionCommitsEachDay(sheetCommitsEachDay, analysis.CommitsEachDay); 41 | 42 | var sheetLinesChangedEachDay = excelPackage.Workbook.Worksheets.Add("Lines changed each day"); 43 | AddSectionLinesChangedEachDay(sheetLinesChangedEachDay, analysis.LinesOfCodeAddedEachDay, analysis.LinesOfCodeDeletedEachDay); 44 | 45 | var sheetCodeAge = excelPackage.Workbook.Worksheets.Add("Code age"); 46 | AddSectionCodeAge(sheetCodeAge, analysis.CodeAge); 47 | 48 | if (analysis.Tags.Any() || analysis.Branches.Any()) 49 | { 50 | var sheetTags = excelPackage.Workbook.Worksheets.Add("Tags and Branches"); 51 | AddSectionTagsAndBranches(sheetTags, analysis.Tags, analysis.Branches); 52 | } 53 | 54 | var sheetNumberOfFilesOfEachType = excelPackage.Workbook.Worksheets.Add("Number of files of each type"); 55 | AddSectionNumberOfFilesOfEachType(sheetNumberOfFilesOfEachType, analysis.FileTypes); 56 | 57 | excelPackage.SaveAs(systemIO.FileInfo($"{ReportFilename}.xlsx")); 58 | } 59 | } 60 | 61 | 62 | private void AddSectionCommitsForEachMonth(ExcelWorksheet sheet) 63 | { 64 | Header(sheet, "Commits for each sub-folder"); 65 | 66 | int rowCounter = 3; 67 | TableHeader(sheet, rowCounter, 1, "Folder", 40); 68 | sheet.Column(1).Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 69 | TableHeader(sheet, rowCounter, 2, "File changes", 13); 70 | sheet.Column(2).Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 71 | TableHeader(sheet, rowCounter, 3, "Percentage", 10); 72 | sheet.Column(3).Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 73 | sheet.Column(3).Style.Numberformat.Format = "#,##0.00%"; 74 | 75 | rowCounter++; 76 | var totalCommits = FileCommitsList.Sum(fc => fc.CommitCount); 77 | foreach (var folder in FolderCommitsList.Take(25)) 78 | { 79 | sheet.Cells[rowCounter, 1].Value = folder.FolderName; 80 | sheet.Cells[rowCounter, 2].Value = FolderCommits[folder.FolderName].FileChanges; 81 | var percentage = (double)FolderCommits[folder.FolderName].FileChanges / (double)totalCommits; 82 | sheet.Cells[rowCounter, 3].Value = percentage; 83 | rowCounter++; 84 | } 85 | var chart = sheet.Drawings.AddChart("Commits each day", OfficeOpenXml.Drawing.Chart.eChartType.Pie); 86 | chart.SetSize(500, 500); 87 | chart.SetPosition(0, 450); 88 | var series1 = chart.Series.Add($"$B$4:$B${rowCounter}", $"$A$4:$A${rowCounter}"); 89 | } 90 | 91 | private void AddSectionCommitsForEachFile(ExcelWorksheet sheet) 92 | { 93 | Header(sheet, $"Top {NumberOfFilesToList} most changed files"); 94 | 95 | int rowCounter = 3; 96 | foreach (var fileChange in FileCommitsList.Take(NumberOfFilesToList)) 97 | { 98 | rowCounter = AddSectionCommitsForFile(sheet, fileChange, rowCounter); 99 | } 100 | } 101 | 102 | private int AddSectionCommitsForFile(ExcelWorksheet sheet, FileStat fileChange, int rowCounter) 103 | { 104 | sheet.Cells[rowCounter, 1].Value = fileChange.Filename; 105 | sheet.Cells[rowCounter, 1].Style.Font.Size = 16; 106 | rowCounter++; 107 | 108 | sheet.Cells[rowCounter, 1].Value = "Latest commit"; 109 | sheet.Cells[rowCounter, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 110 | sheet.Cells[rowCounter, 2].Value = fileChange.LatestCommit; 111 | rowCounter++; 112 | sheet.Cells[rowCounter, 1].Value = "Commits"; 113 | sheet.Cells[rowCounter, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 114 | sheet.Cells[rowCounter, 2].Value = fileChange.CommitCount; 115 | rowCounter++; 116 | if (fileChange.FileExists) 117 | { 118 | var linesOfCode = fileChange.LinesOfCode > 0 ? fileChange.LinesOfCode.ToString() : "N/A"; 119 | sheet.Cells[rowCounter, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 120 | sheet.Cells[rowCounter, 2].Value = linesOfCode; 121 | rowCounter++; 122 | var cyclomaticComplexity = fileChange.CyclomaticComplexity > 0 ? fileChange.CyclomaticComplexity.ToString() : "N/A"; 123 | sheet.Cells[rowCounter, 1].Value = "Cyclomatic complexity"; 124 | sheet.Cells[rowCounter, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 125 | sheet.Cells[rowCounter, 2].Value = cyclomaticComplexity; 126 | rowCounter++; 127 | var methodCount = fileChange.MethodCount > 0 ? fileChange.MethodCount.ToString() : "N/A"; sheet.Cells[rowCounter, 1].Value = "Lines of code"; 128 | sheet.Cells[rowCounter, 1].Value = "Method count"; 129 | sheet.Cells[rowCounter, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 130 | sheet.Cells[rowCounter, 2].Value = methodCount; 131 | rowCounter++; 132 | } 133 | else 134 | { 135 | sheet.Cells[rowCounter, 1].Value = "File has been deleted"; 136 | sheet.Cells[rowCounter, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 137 | rowCounter++; 138 | } 139 | 140 | TableHeader(sheet, rowCounter, 1, "Author", 25); 141 | TableHeader(sheet, rowCounter, 2, "Commits", 9); 142 | TableHeader(sheet, rowCounter, 3, "Percentage", 11); 143 | TableHeader(sheet, rowCounter, 4, "Latest commit", 11); 144 | rowCounter++; 145 | foreach (var userfileChange in UserfileCommitsList.Where(ufc => ufc.Filename == fileChange.Filename)) 146 | { 147 | sheet.Cells[rowCounter, 1].Value = userfileChange.Username; 148 | sheet.Cells[rowCounter, 2].Value = userfileChange.CommitCount; 149 | sheet.Cells[rowCounter, 3].Value = (double)userfileChange.CommitCount / (double)fileChange.CommitCount; 150 | sheet.Cells[rowCounter, 3].Style.Numberformat.Format = "#,##0.00%"; 151 | var commitDatesOrdered = userfileChange.CommitDates.OrderBy(date => date); 152 | sheet.Cells[rowCounter, 4].Value = commitDatesOrdered.Last().ToString("yyyy-MM-dd"); 153 | rowCounter++; 154 | } 155 | 156 | return rowCounter; 157 | } 158 | 159 | 160 | private void AddSectionStatistics(ExcelWorksheet sheet, Analysis analysis) 161 | { 162 | Header(sheet, "Statistics"); 163 | 164 | sheet.Column(1).Width = 24; 165 | sheet.Column(1).Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 166 | sheet.Column(2).Width = 11; 167 | sheet.Column(2).Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Right; 168 | int rowCounter = 3; 169 | sheet.Cells[rowCounter, 1].Value = "First commit"; 170 | sheet.Cells[rowCounter, 2].Value = analysis.FirstCommitDate.ToString("yyyy-MM-dd"); 171 | rowCounter++; 172 | sheet.Cells[rowCounter, 1].Value = "Latest commit"; 173 | sheet.Cells[rowCounter, 2].Value = analysis.LatestCommitDate.ToString("yyyy-MM-dd"); 174 | rowCounter++; 175 | var totalCommits = FileCommitsList.Sum(fc => fc.CommitCount); 176 | sheet.Cells[rowCounter, 1].Value = "Number of commits"; 177 | sheet.Cells[rowCounter, 2].Value = totalCommits; 178 | rowCounter++; 179 | sheet.Cells[rowCounter, 1].Value = "Lines of code analyzed"; 180 | sheet.Cells[rowCounter, 2].Value = analysis.LinesOfCodeanalyzed; 181 | rowCounter++; 182 | var numberOfAuthors = UserfileCommitsList.Select(ufc => ufc.Username).Distinct().Count(); 183 | sheet.Cells[rowCounter, 1].Value = "Number of authors"; 184 | sheet.Cells[rowCounter, 2].Value = numberOfAuthors; 185 | rowCounter++; 186 | sheet.Cells[rowCounter, 1].Value = "Analysis time(milliseconds)"; 187 | sheet.Cells[rowCounter, 2].Value = analysis.AnalysisTime; 188 | rowCounter++; 189 | } 190 | 191 | private void AddSectionCommitsEachDay(ExcelWorksheet sheet, Dictionary commitsEachDay) 192 | { 193 | Header(sheet, "Commits each day"); 194 | 195 | int rowCounter = 3; 196 | TableHeader(sheet, rowCounter, 1, "Date", 11); 197 | TableHeader(sheet, rowCounter, 2, "Commits", 10); 198 | 199 | rowCounter++; 200 | var dateOfFirstChange = commitsEachDay.Keys.OrderBy(date => date).First(); 201 | for (var date = dateOfFirstChange; date <= DateTime.Now; date = date.AddDays(1)) 202 | { 203 | var commits = commitsEachDay.ContainsKey(date) ? commitsEachDay[date] : 0; 204 | sheet.Cells[rowCounter, 1].Value = date.ToString("yyyy-MM-dd"); 205 | sheet.Cells[rowCounter, 2].Value = commits; 206 | rowCounter++; 207 | } 208 | var chart = sheet.Drawings.AddChart("Commits each day", OfficeOpenXml.Drawing.Chart.eChartType.Line); 209 | chart.SetSize(600, 400); 210 | chart.SetPosition(0, 200); 211 | var series1 = chart.Series.Add($"$B$4:$B${rowCounter}", $"$A$4:$A${rowCounter}"); 212 | series1.Header = "Commits"; 213 | chart.Legend.Remove(); 214 | } 215 | 216 | private void AddSectionLinesChangedEachDay(ExcelWorksheet sheet, Dictionary linesOfCodeAddedEachDay, Dictionary linesOfCodeDeletedEachDay) 217 | { 218 | Header(sheet, "Lines changed each day"); 219 | 220 | int rowCounter = 3; 221 | TableHeader(sheet, rowCounter, 1, "Date", 11); 222 | TableHeader(sheet, rowCounter, 2, "Lines Added", 12); 223 | TableHeader(sheet, rowCounter, 3, "Lines deleted", 12); 224 | 225 | rowCounter++; 226 | var dateOfFirstChange = linesOfCodeAddedEachDay.Keys.OrderBy(date => date).First(); 227 | for (var date = dateOfFirstChange; date <= DateTime.Now; date = date.AddDays(1)) 228 | { 229 | var numberOfLinesAdded = linesOfCodeAddedEachDay.ContainsKey(date) ? linesOfCodeAddedEachDay[date] : 0; 230 | var numberOfLinesDeleted = linesOfCodeDeletedEachDay.ContainsKey(date) ? linesOfCodeDeletedEachDay[date] : 0; 231 | sheet.Cells[rowCounter, 1].Value = date.ToString("yyyy-MM-dd"); 232 | sheet.Cells[rowCounter, 2].Value = numberOfLinesAdded; 233 | sheet.Cells[rowCounter, 3].Value = numberOfLinesDeleted; 234 | rowCounter++; 235 | } 236 | var chart = sheet.Drawings.AddChart("LineOfCodeChangeEachDay", OfficeOpenXml.Drawing.Chart.eChartType.Line); 237 | chart.SetSize(600, 400); 238 | chart.SetPosition(0, 250); 239 | var series1 = chart.Series.Add($"$B$4:$B${rowCounter}", $"$A$4:$A${rowCounter}"); 240 | series1.Header = "Added"; 241 | var series2 = chart.Series.Add($"$C$4:$C${rowCounter}", $"$A$4:$A${rowCounter}"); 242 | series2.Header = "Deleted"; 243 | chart.Legend.Position = OfficeOpenXml.Drawing.Chart.eLegendPosition.Top; 244 | } 245 | 246 | private void AddSectionCodeAge(ExcelWorksheet sheet, Dictionary codeAge) 247 | { 248 | Header(sheet, "Code age"); 249 | 250 | int rowCounter = 3; 251 | TableHeader(sheet, rowCounter, 1, "Code age (months)", 14); 252 | TableHeader(sheet, rowCounter, 2, "Filechanges", 12); 253 | 254 | rowCounter++; 255 | var maxAge = codeAge.AsEnumerable().OrderByDescending(kvp => kvp.Key).First().Key; 256 | for(var month = 0; month <= maxAge; month++){ 257 | var fileChanges = codeAge.ContainsKey(month) ? codeAge[month] : 0; 258 | sheet.Cells[rowCounter, 1].Value = month; 259 | sheet.Cells[rowCounter, 2].Value = fileChanges; 260 | rowCounter++; 261 | } 262 | var chart = sheet.Drawings.AddChart("CodeAge", OfficeOpenXml.Drawing.Chart.eChartType.ColumnClustered); 263 | chart.SetSize(600, 400); 264 | chart.SetPosition(0, 250); 265 | var series1 = chart.Series.Add($"$B$4:$B${rowCounter}", $"$A$4:$A${rowCounter}"); 266 | chart.Legend.Remove(); 267 | chart.XAxis.Title.Text = "Code age (months)"; 268 | chart.XAxis.Title.Font.Size = 12; 269 | } 270 | 271 | private void AddSectionTagsAndBranches(ExcelWorksheet sheet, Dictionary tags, List branches) 272 | { 273 | var tagsOrdered = tags.AsEnumerable().OrderByDescending(kvp => kvp.Key).ThenBy(kvp => kvp.Value); 274 | Header(sheet, "Tags"); 275 | 276 | int rowCounter = 3; 277 | TableHeader(sheet, rowCounter, 1, "Tag", 24); 278 | TableHeader(sheet, rowCounter, 2, "Commit date", 11); 279 | 280 | rowCounter++; 281 | foreach (var kvp in tagsOrdered) 282 | { 283 | var tag = kvp.Value; 284 | var tagCommitDate = kvp.Key.ToString("yyyy-MM-dd"); 285 | sheet.Cells[rowCounter, 1].Value = tag; 286 | sheet.Cells[rowCounter, 2].Value = tagCommitDate; 287 | rowCounter++; 288 | } 289 | 290 | Header(sheet, "Branches", 1, 4); 291 | 292 | rowCounter = 3; 293 | TableHeader(sheet, rowCounter, 4, "Branch", 24); 294 | 295 | rowCounter++; 296 | foreach (var branch in branches) 297 | { 298 | sheet.Cells[rowCounter, 4].Value = branch; 299 | rowCounter++; 300 | } 301 | } 302 | 303 | private void AddSectionNumberOfFilesOfEachType(ExcelWorksheet sheet, Dictionary fileTypes) 304 | { 305 | Header(sheet, "Number of files of each type"); 306 | 307 | int rowCounter = 3; 308 | TableHeader(sheet, rowCounter, 1, "File type"); 309 | TableHeader(sheet, rowCounter, 2, "Count"); 310 | 311 | rowCounter++; 312 | var fileTypesOrdered = fileTypes.AsEnumerable().OrderByDescending(kvp => kvp.Value).ThenBy(kvp => kvp.Key); 313 | foreach (var kvp in fileTypesOrdered) 314 | { 315 | sheet.Cells[rowCounter, 1].Value = kvp.Key; 316 | sheet.Cells[rowCounter, 2].Value = kvp.Value; 317 | rowCounter++; 318 | } 319 | } 320 | 321 | private void Header(ExcelWorksheet sheet, string header, int row = 1, int column = 1) 322 | { 323 | sheet.Cells[row, column].Value = header; 324 | sheet.Cells[row, column].Style.Font.Size = 18; 325 | } 326 | 327 | private void TableHeader(ExcelWorksheet sheet, int row, int col, string text, int width = -1) 328 | { 329 | sheet.Cells[row, col].Value = text; 330 | sheet.Cells[row, col].Style.Font.Bold = true; 331 | if (width > 0) 332 | { 333 | sheet.Column(col).Width = width; 334 | } 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Reporting/HTMLReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Net; 6 | using GitCommitsAnalysis.Interfaces; 7 | using GitCommitsAnalysis.Model; 8 | 9 | namespace GitCommitsAnalysis.Reporting 10 | { 11 | public class HTMLReport : BaseReport, IReport 12 | { 13 | public HTMLReport(ISystemIO systemIO, string reportFilename, Options options) : base(systemIO, reportFilename, options) 14 | { 15 | } 16 | 17 | public void Generate(Analysis analysis) 18 | { 19 | Console.WriteLine("Generating HTML report..."); 20 | if (analysis == null) throw new ArgumentException("Parameter analysis is null.", nameof(analysis)); 21 | this.FileCommitsList = analysis.FileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename); 22 | this.UserfileCommitsList = analysis.UserfileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename).ThenBy(fc => fc.Username); 23 | var key = 1; 24 | foreach (var username in analysis.UserfileCommits.Values.Select(fc => fc.Username).Distinct().OrderBy(un => un)) 25 | { 26 | UserNameKey.Add(username, key++); 27 | }; 28 | this.FolderCommits = analysis.FolderCommits; 29 | this.FolderCommitsList = analysis.FolderCommits.Values.OrderByDescending(fc => fc.FileChanges); 30 | 31 | StringBuilder sb = new StringBuilder(); 32 | AddHeader(sb, analysis); 33 | AddNavTabs(sb); 34 | sb.AppendLine("
"); 35 | sb.AppendLine("
"); 36 | AddSectionCommitsForEachFolder(sb); 37 | sb.Append($"

Top {NumberOfFilesToList} most changed files

"); 38 | var sectionCounter = 1; 39 | foreach (var fileChange in FileCommitsList.Take(NumberOfFilesToList)) 40 | { 41 | AddSectionCommitsForEachFile(sb, fileChange, sectionCounter++); 42 | } 43 | sb.AppendLine("
"); 44 | sb.AppendLine("
"); 45 | AddSectionProjectStatistics(sb, analysis); 46 | AddSectionCommitsForEachDay(sb, analysis.CommitsEachDay); 47 | AddSectionLinesChangedEachDay(sb, analysis.LinesOfCodeAddedEachDay, analysis.LinesOfCodeDeletedEachDay); 48 | AddSectionCodeAge(sb, analysis.CodeAge); 49 | if (analysis.Tags.Any()) 50 | { 51 | AddSectionTags(sb, analysis.Tags); 52 | } 53 | if (analysis.Branches.Any()) 54 | { 55 | AddSectionBranches(sb, analysis.Branches); 56 | } 57 | AddSectionFileTypes(sb, analysis.FileTypes); 58 | sb.AppendLine("
"); 59 | sb.AppendLine("
"); 60 | AddFooter(sb); 61 | 62 | SystemIO.WriteAllText($"{ReportFilename}.html", sb.ToString()); 63 | } 64 | 65 | private void AddHeader(StringBuilder sb, Analysis analysis) 66 | { 67 | sb.AppendLine(""); 68 | sb.AppendLine($"{Title}"); 69 | sb.AppendLine(""); 70 | sb.AppendLine(""); 71 | sb.AppendLine(""); 72 | sb.AppendLine(""); 73 | sb.AppendLine(""); 74 | sb.AppendLine(""); 133 | sb.AppendLine(""); 134 | sb.AppendLine(""); 135 | sb.AppendLine("
"); 136 | sb.AppendLine($"

{Title}

"); 137 | sb.AppendLine($"
Report created: {analysis.CreatedDate.ToString("yyyy-MM-dd")}
"); 138 | } 139 | 140 | private void AddFooter(StringBuilder sb) 141 | { 142 | sb.AppendLine("
This report was generated by GitCommitsAnalysis.
"); 143 | sb.AppendLine("
"); 144 | 145 | sb.AppendLine(""); 146 | sb.AppendLine(""); 147 | } 148 | 149 | private void AddNavTabs(StringBuilder sb) 150 | { 151 | sb.AppendLine(""); 155 | sb.AppendLine(""); 159 | } 160 | 161 | private void AddPieChartJavascript(StringBuilder sb) 162 | { 163 | sb.AppendLine(""); 181 | } 182 | 183 | private void AddSectionCommitsForEachFolder(StringBuilder sb) 184 | { 185 | var totalCommits = FileCommitsList.Sum(fc => fc.CommitCount); 186 | sb.AppendLine("
"); 187 | sb.AppendLine("
"); 188 | sb.AppendLine("

Commits for each sub-folder

"); 189 | sb.AppendLine(""); 190 | sb.AppendLine(""); 191 | int folderCounter = 1; 192 | foreach (var folder in FolderCommitsList.Take(25)) 193 | { 194 | var changeCount = string.Format("{0,5}", FolderCommits[folder.FolderName].FileChanges); 195 | var percentage = string.Format("{0,5:#0.0}", ((double)FolderCommits[folder.FolderName].FileChanges / (double)totalCommits) * 100); 196 | var expand = folder.Children.Keys.Count > 0 ? $"Expand" : ""; 197 | sb.AppendLine($""); 198 | if (folder.Children.Keys.Count > 0) 199 | { 200 | sb.AppendLine($""); 205 | } 206 | folderCounter++; 207 | } 208 | var total = string.Format("{0,5}", totalCommits); 209 | sb.AppendLine($""); 210 | sb.AppendLine("
FolderFile changes
{WebUtility.HtmlEncode(folder.FolderName)}{changeCount} ({percentage}%){expand}
"); 201 | sb.AppendLine($"
    "); 202 | AddSectionCommitsForEachFolderChildren(sb, folder, 0); 203 | sb.AppendLine("
"); 204 | sb.AppendLine("
Total number of Commits analyzed{total} ({string.Format("{0,5:##0.0}", 100)}%)
\n"); 211 | sb.AppendLine("
"); 212 | sb.AppendLine("
"); 213 | AddPieChartJavascript(sb); 214 | sb.AppendLine("
"); 215 | sb.AppendLine(""); 236 | sb.AppendLine("
"); 237 | } 238 | 239 | private void AddSectionCommitsForEachFolderChildren(StringBuilder sb, FolderStat parentFolder, int indent) 240 | { 241 | if (parentFolder.Children.Keys.Count > 0) 242 | { 243 | foreach (var folder in parentFolder.Children.Values.OrderByDescending(fs => fs.FileChanges)) 244 | { 245 | var changeCount = string.Format("{0,5}", parentFolder.Children[folder.FolderName].FileChanges); 246 | var icon = FileIcon(folder.FolderName); 247 | var folderName = folder.Children.Keys.Count > 0 ? $"{WebUtility.HtmlEncode(folder.FolderName)}" : $"{WebUtility.HtmlEncode(folder.FolderName)}"; 248 | var padding = folder.Children.Keys.Count <= 0 ? "pl40" : "pl20"; 249 | sb.AppendLine($"
  • {folderName}: {changeCount}"); 250 | if (folder.Children.Keys.Count > 0) 251 | { 252 | sb.AppendLine($"
      "); 253 | AddSectionCommitsForEachFolderChildren(sb, folder, indent + 1); 254 | sb.AppendLine("
    "); 255 | } 256 | sb.AppendLine("
  • "); 257 | } 258 | } 259 | } 260 | 261 | private string FileIcon(string filename) 262 | { 263 | var extension = SystemIO.GetExtension(filename); 264 | if (!string.IsNullOrEmpty(extension)) 265 | { 266 | extension = extension.Substring(1); 267 | } 268 | var cssClass = ""; 269 | switch (extension) 270 | { 271 | case "html": 272 | case "cshtml": 273 | case "ts": 274 | case "cs": 275 | case "ps1": 276 | case "bat": 277 | case "cmd": 278 | case "sh": 279 | case "json": 280 | case "xml": 281 | case "css": 282 | case "scss": 283 | cssClass += "far fa-file-code"; 284 | break; 285 | case "js": 286 | cssClass += "fab fa-js"; 287 | break; 288 | case "xls": 289 | case "xlsx": 290 | cssClass += "far fa-file-excel"; 291 | break; 292 | case "csv": 293 | cssClass += "far fa-file-csv"; 294 | break; 295 | case "doc": 296 | case "docx": 297 | cssClass += "far fa-file-word"; 298 | break; 299 | case "pdf": 300 | cssClass += "far fa-file-pdf"; 301 | break; 302 | case "jpg": 303 | case "jpeg": 304 | case "png": 305 | case "gif": 306 | case "svg": 307 | case "tiff": 308 | case "tif": 309 | case "bmp": 310 | case "ico": 311 | cssClass += "far fa-file-image"; 312 | break; 313 | case "txt": 314 | cssClass += "far fa-file-alt"; 315 | break; 316 | case "md": 317 | cssClass += "fab fa-markdown"; 318 | break; 319 | case "zip": 320 | case "tgz": 321 | case "tar": 322 | case "rar": 323 | cssClass += "far fa-file-archive"; 324 | break; 325 | case "eot": 326 | case "otf": 327 | case "ttf": 328 | case "woff": 329 | case "woff2": 330 | cssClass += "fas fa-font"; 331 | break; 332 | default: 333 | cssClass += "far fa-file"; 334 | break; 335 | } 336 | return cssClass; 337 | } 338 | 339 | private void AddSectionProjectStatistics(StringBuilder sb, Analysis analysis) 340 | { 341 | var totalCommits = FileCommitsList.Sum(fc => fc.CommitCount); 342 | var numberOfAuthors = UserfileCommitsList.Select(ufc => ufc.Username).Distinct().Count(); 343 | sb.AppendLine("
    "); 344 | sb.AppendLine("
    "); 345 | sb.AppendLine(""); 346 | sb.AppendLine($""); 347 | sb.AppendLine($""); 348 | sb.AppendLine($""); 349 | sb.AppendLine($""); 350 | sb.AppendLine($""); 351 | sb.AppendLine($""); 352 | sb.AppendLine("
    First commit{analysis.FirstCommitDate.ToString("yyyy-MM-dd")}
    Latest commit{analysis.LatestCommitDate.ToString("yyyy-MM-dd")}
    Number of commits{totalCommits}
    Lines of code analyzed{analysis.LinesOfCodeanalyzed}
    Number of authors{numberOfAuthors}
    Analysis time(milliseconds){analysis.AnalysisTime}
    "); 353 | sb.AppendLine("
    "); 354 | } 355 | 356 | private void AddCommitsEachDayChartJavascript(StringBuilder sb, Dictionary commitsEachDay) 357 | { 358 | sb.AppendLine(""); 382 | } 383 | 384 | private void AddSectionCommitsForEachDay(StringBuilder sb, Dictionary commitsEachDay) 385 | { 386 | sb.AppendLine("
    "); 387 | sb.AppendLine("
    "); 388 | sb.AppendLine("

    Commits for each day

    "); 389 | AddCommitsEachDayChartJavascript(sb, commitsEachDay); 390 | sb.AppendLine("
    "); 391 | sb.AppendLine("
    "); 392 | } 393 | 394 | private void AddLinesChangedEachDayChartJavascript(StringBuilder sb, Dictionary linesOfCodeAddedEachDay, Dictionary linesOfCodeDeletedEachDay) 395 | { 396 | sb.AppendLine(""); 422 | } 423 | 424 | private void AddSectionLinesChangedEachDay(StringBuilder sb, Dictionary linesOfCodeAddedEachDay, Dictionary linesOfCodeDeletedEachDay) 425 | { 426 | sb.AppendLine("
    "); 427 | sb.AppendLine("
    "); 428 | sb.AppendLine("

    Lines changed each day

    "); 429 | sb.AppendLine(); 430 | AddLinesChangedEachDayChartJavascript(sb, linesOfCodeAddedEachDay, linesOfCodeDeletedEachDay); 431 | sb.AppendLine("
    "); 432 | sb.AppendLine("
    "); 433 | } 434 | 435 | private void AddCodeAgeChartJavascript(StringBuilder sb, Dictionary codeAge) 436 | { 437 | sb.AppendLine(""); 462 | } 463 | 464 | private void AddSectionCodeAge(StringBuilder sb, Dictionary codeAge) 465 | { 466 | sb.AppendLine("
    "); 467 | sb.AppendLine("
    "); 468 | sb.AppendLine("

    Code age

    "); 469 | sb.AppendLine(); 470 | AddCodeAgeChartJavascript(sb, codeAge); 471 | sb.AppendLine("
    "); 472 | sb.AppendLine("
    "); 473 | } 474 | 475 | private void AddSectionFileTypes(StringBuilder sb, Dictionary fileTypes) 476 | { 477 | var fileTypesOrdered = fileTypes.AsEnumerable().OrderByDescending(kvp => kvp.Value).ThenBy(kvp => kvp.Key); 478 | sb.AppendLine("
    "); 479 | sb.AppendLine("
    "); 480 | sb.AppendLine("

    Number of files of each type

    "); 481 | sb.AppendLine(""); 482 | sb.AppendLine(""); 483 | int rowCounter = 1; 484 | foreach (var kvp in fileTypesOrdered) 485 | { 486 | var icon = FileIcon("a" + kvp.Key); 487 | sb.AppendLine($""); 488 | if (rowCounter++ % 10 == 0) 489 | { 490 | sb.AppendLine("
    File typeCount
    {WebUtility.HtmlEncode(kvp.Key)}{kvp.Value}
    "); 491 | sb.AppendLine(""); 492 | sb.AppendLine(""); 493 | } 494 | } 495 | sb.AppendLine("
    File typeCount
    "); 496 | sb.AppendLine("
    "); 497 | } 498 | 499 | private void AddSectionCommitsForEachFile(StringBuilder sb, FileStat fileChange, int sectionCounter) 500 | { 501 | sb.AppendLine("
    "); 502 | sb.AppendLine($"

    {WebUtility.HtmlEncode(fileChange.Filename)}

    "); 503 | sb.AppendLine(""); 504 | sb.AppendLine($""); 505 | sb.AppendLine($""); 506 | if (fileChange.FileExists) 507 | { 508 | var linesOfCode = fileChange.LinesOfCode > 0 ? fileChange.LinesOfCode.ToString() : "N/A"; 509 | var cyclomaticComplexity = fileChange.CyclomaticComplexity > 0 ? fileChange.CyclomaticComplexity.ToString() : "N/A"; 510 | var methodCount = fileChange.MethodCount > 0 ? fileChange.MethodCount.ToString() : "N/A"; 511 | sb.AppendLine($""); 512 | sb.AppendLine($""); 513 | sb.AppendLine($""); 514 | } 515 | else 516 | { 517 | sb.AppendLine($""); 518 | } 519 | sb.AppendLine("
    Latest commit{fileChange.LatestCommit.ToString("yyyy-MM-dd")}
    Commits{fileChange.CommitCount}
    Lines of code{linesOfCode}
    Cyclomatic Complexity{cyclomaticComplexity}
    Method count{methodCount}
    File has been deleted
    "); 520 | sb.AppendLine("
    "); 521 | sb.AppendLine("
    "); 522 | sb.AppendLine("
    "); 523 | sb.AppendLine("
    "); 524 | 525 | sb.AppendLine("
    "); 526 | sb.AppendLine(""); 527 | sb.AppendLine($""); 528 | var commitDates = new List(); 529 | foreach (var userfileChange in UserfileCommitsList.Where(ufc => ufc.Filename == fileChange.Filename)) 530 | { 531 | var username = WebUtility.HtmlEncode(userfileChange.Username); 532 | var changeCount = string.Format("{0,3}", userfileChange.CommitCount); 533 | var percentage = string.Format("{0,5:#0.00}", ((double)userfileChange.CommitCount / (double)fileChange.CommitCount) * 100); 534 | var latestCommit = GenerateScatterPlotData(commitDates, userfileChange); 535 | sb.AppendLine($""); 536 | } 537 | 538 | sb.AppendLine("
    NameCommitsPercentageLatest commit
    {username}{changeCount}{percentage}%{latestCommit}
    "); 539 | sb.AppendLine("
    "); 540 | 541 | sb.AppendLine("
    "); 542 | AddScatterplotJavascript(sb, commitDates, sectionCounter); 543 | sb.AppendLine($"
    "); 544 | sb.AppendLine("
    "); 545 | sb.AppendLine("
    "); 546 | } 547 | 548 | private string GenerateScatterPlotData(List commitDates, FileStat fileStat) 549 | { 550 | var commitDatesOrdered = fileStat.CommitDates.OrderBy(date => date); 551 | foreach (var commitDate in commitDatesOrdered) 552 | { 553 | var date = $"new Date('{commitDate.ToString("yyyy-MM-dd")}')"; 554 | var userId = UserNameKey[fileStat.Username]; 555 | if (!commitDates.Any(cd => cd.Date == date && cd.UserId == userId)) // Only add a point for each user for each date 556 | { 557 | commitDates.Add(new ScatterPoint 558 | { 559 | Date = date, 560 | UserId = userId, 561 | ToolTip = $"'{commitDate.ToString("yyyy-MM-dd")}, {fileStat.Username}'" 562 | }); 563 | } 564 | } 565 | var latestCommitDate = commitDatesOrdered.Last(); 566 | return latestCommitDate.ToString("yyyy-MM-dd"); 567 | } 568 | 569 | private static void AddScatterplotJavascript(StringBuilder sb, List commitDates, int sectionCounter) 570 | { 571 | var commitDatesString = string.Join(",", commitDates.Select(sp => sp.ToString()).OrderBy(sp => sp)); 572 | 573 | sb.AppendLine(""); 592 | } 593 | 594 | private void AddSectionTags(StringBuilder sb, Dictionary tags) 595 | { 596 | var tagsOrdered = tags.AsEnumerable().OrderByDescending(kvp => kvp.Key).ThenBy(kvp => kvp.Value); 597 | sb.AppendLine("
    "); 598 | sb.AppendLine("
    "); 599 | sb.AppendLine("

    Tags

    "); 600 | sb.AppendLine(""); 601 | sb.AppendLine(""); 602 | var tagDates = new List(); 603 | foreach (var kvp in tagsOrdered) 604 | { 605 | var tag = kvp.Value; 606 | var tagCommitDate = kvp.Key.ToString("yyyy-MM-dd"); 607 | sb.AppendLine($""); 608 | var date = $"new Date('{tagCommitDate}')"; 609 | tagDates.Add(new ScatterPoint 610 | { 611 | Date = date, 612 | UserId = 1, 613 | ToolTip = $"'{tag}, {tagCommitDate}'" 614 | }); 615 | } 616 | sb.AppendLine("
    TagCommit date
    {WebUtility.HtmlEncode(tag)}{tagCommitDate}
    "); 617 | sb.AppendLine("
    "); 618 | sb.AppendLine("
    "); 619 | AddTagsScatterplotJavascript(sb, tagDates); 620 | sb.AppendLine("
    "); 621 | sb.AppendLine("
    "); 622 | } 623 | 624 | private static void AddTagsScatterplotJavascript(StringBuilder sb, List tagDates) 625 | { 626 | var tagDatesString = string.Join(",", tagDates.Select(sp => sp.ToString()).OrderBy(sp => sp)); 627 | 628 | sb.AppendLine(""); 648 | } 649 | 650 | private void AddSectionBranches(StringBuilder sb, List branches) 651 | { 652 | sb.AppendLine("
    "); 653 | sb.AppendLine("
    "); 654 | sb.AppendLine("

    Branches

    "); 655 | sb.AppendLine(""); 656 | sb.AppendLine(""); 657 | foreach (var branch in branches) 658 | { 659 | sb.Append($""); 660 | } 661 | sb.AppendLine("
    Name
    {branch}
    "); 662 | sb.AppendLine("
    "); 663 | } 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Reporting/JsonReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | using GitCommitsAnalysis.Interfaces; 4 | using GitCommitsAnalysis.Model; 5 | 6 | namespace GitCommitsAnalysis.Reporting 7 | { 8 | public class JsonReport : IReport 9 | { 10 | private string reportFilename; 11 | private string title; 12 | private ISystemIO systemIO; 13 | public JsonReport(ISystemIO systemIO, string reportFilename, Options options) 14 | { 15 | this.reportFilename = reportFilename; 16 | this.title = options.Title; 17 | this.systemIO = systemIO; 18 | } 19 | 20 | public void Generate(Analysis analysis) 21 | { 22 | Console.WriteLine("Generating Json file..."); 23 | systemIO.WriteAllText($"{reportFilename}.json", JsonConvert.SerializeObject(new { 24 | title, 25 | analysis 26 | })); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Reporting/MarkdownReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using GitCommitsAnalysis.Interfaces; 5 | using GitCommitsAnalysis.Model; 6 | 7 | namespace GitCommitsAnalysis.Reporting 8 | { 9 | 10 | public class MarkdownReport : BaseReport, IReport 11 | { 12 | public MarkdownReport(ISystemIO systemIO, string reportFilename, Options options) : base(systemIO, reportFilename, options) 13 | { 14 | } 15 | 16 | public void Generate(Analysis analysis) 17 | { 18 | Console.WriteLine("Generating Markdown report..."); 19 | StringBuilder sb = new StringBuilder(); 20 | FileCommitsList = analysis.FileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename); 21 | UserfileCommitsList = analysis.UserfileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename).ThenBy(fc => fc.Username); 22 | 23 | sb.AppendLine($"# {Title}\n"); 24 | sb.AppendLine($"Report created: {analysis.CreatedDate.ToString("yyyy-MM-dd")}\n"); 25 | 26 | var totalCommits = FileCommitsList.Sum(fc => fc.CommitCount); 27 | var numberOfAuthors = UserfileCommitsList.Select(ufc => ufc.Username).Distinct().Count(); 28 | sb.AppendLine($"## Statistics\n"); 29 | sb.AppendLine("| | |"); 30 | sb.AppendLine("|---:|----:|"); 31 | sb.AppendLine($"| First commit | {analysis.FirstCommitDate.ToString("yyyy-MM-dd")} |"); 32 | sb.AppendLine($"| Latest commit | {analysis.LatestCommitDate.ToString("yyyy-MM-dd")} |"); 33 | sb.AppendLine($"| Number of commits | {totalCommits} |"); 34 | sb.AppendLine($"| Lines of code analyzed | {analysis.LinesOfCodeanalyzed} |"); 35 | sb.AppendLine($"| Number of authors | {numberOfAuthors} |"); 36 | sb.AppendLine($"| Analysis time(milliseconds) | {analysis.AnalysisTime} |"); 37 | sb.AppendLine(); 38 | 39 | sb.AppendLine("## File changes for each sub-folder"); 40 | var folderCommitsList = analysis.FolderCommits.Values.OrderByDescending(fc => fc.FileChanges); 41 | foreach (var folder in folderCommitsList.Take(NumberOfFilesToList)) 42 | { 43 | var folderName = string.Format("{0,50}", folder.FolderName); 44 | var changeCount = string.Format("{0,5}", analysis.FolderCommits[folder.FolderName].FileChanges); 45 | var percentage = string.Format("{0,5:#0.00}", ((double)analysis.FolderCommits[folder.FolderName].FileChanges / (double)totalCommits) * 100); 46 | sb.AppendLine($"{folderName}: {changeCount} ({percentage}%)"); 47 | } 48 | sb.AppendLine($"{string.Format("{0,51}", "---------------")} {string.Format("{0,6}", "-----")} {string.Format("{0,7}", "------")}"); 49 | var total = string.Format("{0,5}", totalCommits); 50 | sb.AppendLine($"{string.Format("{0,50}", "Total number of Commits analyzed")}: {total} ({string.Format("{0,5:##0.0}", 100)}%)\n"); 51 | 52 | sb.AppendLine("---\n"); 53 | 54 | foreach (var fileChange in FileCommitsList.Take(50)) 55 | { 56 | var linesOfCode = fileChange.LinesOfCode > 0 ? fileChange.LinesOfCode.ToString() : "N/A"; 57 | var cyclomaticComplexity = fileChange.CyclomaticComplexity > 0 ? fileChange.CyclomaticComplexity.ToString() : "N/A"; 58 | var methodCount = fileChange.MethodCount > 0 ? fileChange.MethodCount.ToString() : "N/A"; 59 | sb.AppendLine($"### {fileChange.Filename}\n"); 60 | sb.AppendLine("| | |"); 61 | sb.AppendLine("|---:|----:|"); 62 | sb.AppendLine($"| Latest commit | {fileChange.LatestCommit.ToString("yyyy-MM-dd")} |"); 63 | sb.AppendLine($"| Commits | {fileChange.CommitCount} |"); 64 | if (fileChange.FileExists) 65 | { 66 | sb.AppendLine($"| Lines of code | {linesOfCode} |"); 67 | sb.AppendLine($"| Cyclomatic Complexity | {cyclomaticComplexity} |"); 68 | sb.AppendLine($"| Method count | {methodCount} |"); 69 | } 70 | else 71 | { 72 | sb.AppendLine($"| File has been deleted | |"); 73 | } 74 | sb.AppendLine(); 75 | 76 | sb.AppendLine("__Commits by user:__\n"); 77 | sb.AppendLine($"| Name | Commits | Percentage |"); 78 | sb.AppendLine($"|-----:|--------:|-----------:|"); 79 | foreach (var userfileChange in UserfileCommitsList.Where(ufc => ufc.Filename == fileChange.Filename)) 80 | { 81 | var username = string.Format("{0,20}", userfileChange.Username); 82 | var changeCount = string.Format("{0,3}", userfileChange.CommitCount); 83 | var percentage = string.Format("{0,5:#0.00}", ((double)userfileChange.CommitCount / (double)fileChange.CommitCount) * 100); 84 | sb.AppendLine($"| {username} | {changeCount} | {percentage}% |"); 85 | } 86 | sb.AppendLine(); 87 | } 88 | 89 | sb.AppendLine("## Code age\n"); 90 | sb.AppendLine("| Code age(months) | Filechanges |"); 91 | sb.AppendLine("|---:|---:|"); 92 | var maxAge = analysis.CodeAge.AsEnumerable().OrderByDescending(kvp => kvp.Key).First().Key; 93 | for (var month = 0; month <= maxAge; month++) 94 | { 95 | var fileChanges = analysis.CodeAge.ContainsKey(month) ? analysis.CodeAge[month] : 0; 96 | sb.AppendLine($"|{month}|{fileChanges}|"); 97 | } 98 | sb.AppendLine(); 99 | 100 | if (analysis.Tags.Any()) 101 | { 102 | var tagsOrdered = analysis.Tags.AsEnumerable().OrderByDescending(kvp => kvp.Key).ThenBy(kvp => kvp.Value); 103 | sb.AppendLine("## Tags\n"); 104 | sb.AppendLine("| Name | Date |"); 105 | sb.AppendLine("|:---|----:|"); 106 | foreach (var kvp in tagsOrdered) 107 | { 108 | sb.AppendLine($"| {kvp.Value} | {kvp.Key.ToString("yyyy-MM-dd")} |"); 109 | } 110 | sb.AppendLine(); 111 | } 112 | 113 | if (analysis.Branches.Any()) 114 | { 115 | sb.AppendLine("## Branches\n"); 116 | sb.AppendLine("| Name |"); 117 | sb.AppendLine("|:---|"); 118 | foreach (var branch in analysis.Branches) 119 | { 120 | sb.AppendLine($"| {branch} |"); 121 | } 122 | sb.AppendLine(); 123 | } 124 | 125 | var fileTypesOrdered = analysis.FileTypes.AsEnumerable().OrderByDescending(kvp => kvp.Value).ThenBy(kvp => kvp.Key); 126 | sb.AppendLine("## Number of files of each type\n"); 127 | sb.AppendLine("| File type | Count |"); 128 | sb.AppendLine("|---:|----:|"); 129 | foreach (var kvp in fileTypesOrdered) 130 | { 131 | sb.AppendLine($"| {kvp.Key} | {kvp.Value} |"); 132 | } 133 | 134 | SystemIO.WriteAllText($"{ReportFilename}.md", sb.ToString()); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/Reporting/TextFileReport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using GitCommitsAnalysis.Interfaces; 5 | using GitCommitsAnalysis.Model; 6 | 7 | namespace GitCommitsAnalysis.Reporting 8 | { 9 | public class TextFileReport : BaseReport, IReport 10 | { 11 | public TextFileReport(ISystemIO systemIO, string reportFilename, Options options) : base(systemIO, reportFilename, options) 12 | { 13 | } 14 | 15 | public void Generate(Analysis analysis) 16 | { 17 | Console.WriteLine("Generating Text file report..."); 18 | StringBuilder sb = new StringBuilder(); 19 | FileCommitsList = analysis.FileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename); 20 | UserfileCommitsList = analysis.UserfileCommits.Values.OrderByDescending(fc => fc.CommitCount).ThenBy(fc => fc.Filename).ThenBy(fc => fc.Username); 21 | 22 | var totalCommits = FileCommitsList.Sum(fc => fc.CommitCount); 23 | var numberOfAuthors = UserfileCommitsList.Select(ufc => ufc.Username).Distinct().Count(); 24 | sb.AppendLine($"{Title}\n"); 25 | sb.AppendLine($"Report created: {analysis.CreatedDate.ToString("yyyy-MM-dd")}"); 26 | sb.AppendLine($"First commit: {analysis.FirstCommitDate.ToString("yyyy-MM-dd")}"); 27 | sb.AppendLine($"Latest commit: {analysis.LatestCommitDate.ToString("yyyy-MM-dd")}"); 28 | sb.AppendLine($"Number of commits: {totalCommits}"); 29 | sb.AppendLine($"Lines of code analyzed: {analysis.LinesOfCodeanalyzed}"); 30 | sb.AppendLine($"Number of authors: {numberOfAuthors}"); 31 | sb.AppendLine($"Analysis time(milliseconds): {analysis.AnalysisTime}"); 32 | sb.AppendLine(); 33 | 34 | var folderCommitsList = analysis.FolderCommits.Values.OrderByDescending(fc => fc.FileChanges); 35 | foreach (var folder in folderCommitsList.Take(NumberOfFilesToList)) 36 | { 37 | var folderName = string.Format("{0,50}", folder.FolderName); 38 | var changeCount = string.Format("{0,5}", analysis.FolderCommits[folder.FolderName].FileChanges); 39 | var percentage = string.Format("{0,5:#0.00}", ((double)analysis.FolderCommits[folder.FolderName].FileChanges / (double)totalCommits) * 100); 40 | sb.AppendLine($"{folderName}: {changeCount} ({percentage}%)"); 41 | } 42 | 43 | foreach (var fileChange in FileCommitsList.Take(50)) 44 | { 45 | sb.AppendLine(""); 46 | string fileInfo = "File has been deleted"; 47 | if (fileChange.FileExists) { 48 | var linesOfCode = fileChange.LinesOfCode > 0 ? fileChange.LinesOfCode.ToString() : "N/A"; 49 | var cyclomaticComplexity = fileChange.CyclomaticComplexity > 0 ? fileChange.CyclomaticComplexity.ToString() : "N/A"; 50 | var methodCount = fileChange.MethodCount > 0 ? fileChange.MethodCount.ToString() : "N/A"; 51 | fileInfo = $"Lines of code: {linesOfCode} - Cyclomatic Complexity: {cyclomaticComplexity} - Method count: {methodCount}"; 52 | } 53 | sb.AppendLine($"{fileChange.Filename}: {fileChange.CommitCount} - Latest commit: {fileChange.LatestCommit.ToString("yyyy-MM-dd")} - {fileInfo}\n"); 54 | foreach (var userfileChange in UserfileCommitsList.Where(ufc => ufc.Filename == fileChange.Filename)) 55 | { 56 | var username = string.Format("{0,20}", userfileChange.Username); 57 | var changeCount = string.Format("{0,3}", userfileChange.CommitCount); 58 | var percentage = string.Format("{0,5:#0.00}", ((double)userfileChange.CommitCount / (double)fileChange.CommitCount) * 100); 59 | sb.AppendLine($" {username}: {changeCount} ({percentage}%)"); 60 | } 61 | } 62 | 63 | SystemIO.WriteAllText($"{ReportFilename}.txt", sb.ToString()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GitCommitsAnalysis/SystemIO.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using GitCommitsAnalysis.Interfaces; 3 | 4 | namespace GitCommitsAnalysis 5 | { 6 | public class SystemIO : ISystemIO 7 | { 8 | public bool FileExists(string filename) 9 | { 10 | return File.Exists(filename); 11 | } 12 | 13 | public string ReadFileContent(string filename) 14 | { 15 | return File.ReadAllText(filename); 16 | } 17 | 18 | public void WriteAllText(string filename, string contents) 19 | { 20 | File.WriteAllText(filename, contents); 21 | } 22 | 23 | public string GetPathWitoutExtension(string filename) 24 | { 25 | return Path.GetFileNameWithoutExtension(filename); 26 | } 27 | 28 | public string GetExtension(string filename) 29 | { 30 | return Path.GetExtension(filename); 31 | } 32 | 33 | public FileInfo FileInfo(string filename) 34 | { 35 | return new FileInfo(filename); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Allan Simonsen 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitCommitsAnalysis ![GitHub top language](https://img.shields.io/github/languages/top/CoderAllan/GitCommitsAnalysis.svg) ![GitHub](https://img.shields.io/github/license/CoderAllan/GitCommitsAnalysis.svg) ![GitHub last commit](https://img.shields.io/github/last-commit/CoderAllan/GitCommitsAnalysis.svg) 2 | 3 | This tool can be used to generate a report showing the temporal distribution of you commits and the size and complexity of the codefiles in a Git repository. 4 | 5 | ## Motivation 6 | 7 | The inspiration for this tool is this YouTube video: [Adam Tornhill - Seven Secrets of Maintainable Codebases](https://www.youtube.com/watch?v=a74UkJxKWVM&t=881s). 8 | In the video he shows how to do a temporal analysis of your codebase to determine hotspots and candidates for refactoring. 9 | 10 | By combining the number of lines of the code files with the number of commits and the [cyclomatic complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity), you can identify codefiles that are candidates for refactoring. 11 | The ideas Adam Tornhill is presenting is that: 12 | 13 | * If a codefile is long (has many lines of code) then there is a good chance that it should be refactored into smaller pieces. 14 | * If a codefile has a large cyclomatic complexity value, then there is a good chance that it should be refactored into multiple pieces with more specific responsibilities. 15 | * If a codefile is changed often, then it is probably an important part of the business logic of the codebase. 16 | * If multiple codefiles are changed on the same dates, then they are probably tightly coupled and maybe share functionality that could be extracted into a super class shared by the candidate files. 17 | 18 | By combining the factors you can identify the candidates where refactoring would improve the quality of your codebase the most. 19 | 20 | ## The tool 21 | 22 | The tool is a command line tool that generates a report in one of the formats: 23 | 24 | * HTML 25 | * Excel 26 | * Markdown 27 | * Text 28 | * Json (A dump of the collected data. Could be used as input for other tools) 29 | 30 | The HTML report is the most detailed with various charts visualizing the statistics. 31 | 32 | ## Commandline parameters 33 | 34 | ```text 35 | GitCommitsAnalysis 1.0.0 36 | Copyright (C) 2019 GitCommitsAnalysis 37 | 38 | -r, --rootfolder Required. The root folder of the application 39 | source code 40 | 41 | -o, --outputfolder Required. The output folder where the generated 42 | reports will be placed 43 | 44 | -a, --reportfilename The filename the report(s) will be given 45 | 46 | -f, --outputformat Required. The output format(s) to generate. 47 | Multiple formats should be space-separated. Eg. 48 | '-f Text Json'. Valid formats: HTML, Markdown, 49 | Json, Text, Excel 50 | 51 | -n, --numberoffilestolist (Default: 50) Specifies the number of flies to 52 | include in the list of most changes files. 53 | (Ignored when output is Json) 54 | 55 | -t, --title (Default: GitCommitsAnalysis) The title to appear 56 | in the top of the reports 57 | 58 | -i, --ignoredfiletypes The file types to ignore when analyzing the Git 59 | repository.. Eg. '-i csproj npmrc gitignore' 60 | ``` 61 | 62 | ## Credits 63 | 64 | The calculation of the Cyclomatic Complexity i found over at [Jakob Reimers ArchiMetrics](https://github.com/jjrdk/ArchiMetrics) repository. 65 | 66 | ## Contributing 67 | 68 | Any ideas, bug reports or help building this tool, is greatly appreciated. Have a look in the [Contributing file](CONTRIBUTING.md) about how to help. 69 | 70 | ## Screenshots 71 | 72 | ![Html report](screenshots/HtmlReport1.png) 73 | 74 | ![Html report](screenshots/HtmlReport2.png) 75 | 76 | ![Html report](screenshots/HtmlReport3.png) 77 | 78 | ![Html report](screenshots/HtmlReport4.png) 79 | 80 | ## Packages used 81 | 82 | [CommandLineParser NuGet package](https://www.nuget.org/packages/CommandLineParser/) for parsing the commandline parameters and generating the help page. 83 | 84 | [LibGit2Sharp](https://www.nuget.org/packages/LibGit2Sharp/) for reading the Git repository. 85 | 86 | [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json/) for generating the Json dump of the analysis data. 87 | 88 | [EPPlus](https://www.nuget.org/packages/EPPlus/) for generating the Excel report. 89 | 90 | [TypeScriptAST](https://www.nuget.org/packages/TypeScriptAST/) for counting methods and functions in Typescript files. 91 | 92 | [Google charts](https://developers.google.com/chart) for displaying the pie chart and the scatter charts in the HTML report. 93 | 94 | [Bootstrap](https://getbootstrap.com/docs/3.4/getting-started/) for styling the HTML report. 95 | 96 | [jQuery](https://jquery.com/) used by the HTML report. 97 | 98 | [FontAwesome](https://fontawesome.com/) used to add icons for the file changes. 99 | -------------------------------------------------------------------------------- /screenshots/HtmlReport1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoderAllan/GitCommitsAnalysis/3ef932e119a2c8c43a77b252810012a3f9d3f600/screenshots/HtmlReport1.png -------------------------------------------------------------------------------- /screenshots/HtmlReport2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoderAllan/GitCommitsAnalysis/3ef932e119a2c8c43a77b252810012a3f9d3f600/screenshots/HtmlReport2.png -------------------------------------------------------------------------------- /screenshots/HtmlReport3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoderAllan/GitCommitsAnalysis/3ef932e119a2c8c43a77b252810012a3f9d3f600/screenshots/HtmlReport3.png -------------------------------------------------------------------------------- /screenshots/HtmlReport4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoderAllan/GitCommitsAnalysis/3ef932e119a2c8c43a77b252810012a3f9d3f600/screenshots/HtmlReport4.png --------------------------------------------------------------------------------