├── makerelease.ps1
├── kbgit.tests
├── XunitConfiguration.cs
├── kbgit.tests.csproj
├── ReadmeHelper.cs
├── RepoBuilder.cs
└── KBGitTests.cs
├── ISSUE_TEMPLATE.md
├── CONTRIBUTING.md
├── Program.cs
├── kbgithelper.ps1
├── KBGit.csproj
├── README.md
├── KBGit.sln
├── .gitignore
├── LICENSE
└── Git.cs
/makerelease.ps1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kbilsted/KBGit/HEAD/makerelease.ps1
--------------------------------------------------------------------------------
/kbgit.tests/XunitConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | [assembly: CollectionBehavior(DisableTestParallelization = true, MaxParallelThreads = 1)]
4 |
5 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Feel free to raise comments, ask questions etc. Just raise an issues.
2 |
3 | If you have suggestions for feature implementations feel free to raise an issue or straight up submit a PR! :-)
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Feel free to raise comments, ask questions etc. Just raise an issues.
2 |
3 | If you have suggestions for feature implementations feel free to raise an issue or straight up submit a PR! :-)
4 |
5 | Feel more than welcome to submit contributions to the documentation of the implementation too ;)
6 |
7 |
8 | cheers!
9 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace KbgSoft.KBGit
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | var output = new CommandLineHandling().Handle(new KBGit(new DirectoryInfo(".").FullName), CommandLineHandling.Config, args);
11 | Console.WriteLine(output);
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/kbgithelper.ps1:
--------------------------------------------------------------------------------
1 |
2 | function CreateGitWithCommits()
3 | {
4 | $git = "C:\src\KBGit\bin\Debug\netcoreapp2.0\win10-x64\KBGit.exe"
5 | "init"
6 | iex "$git init"
7 |
8 | "adding a"
9 | & echo "aaaa" > a.txt
10 | iex "$git commit -m 'file a.txt'"
11 |
12 | "adding b"
13 | & echo "bbbb" > b.txt
14 | iex "$git commit -m 'b file'"
15 |
16 | iex "$git log"
17 | iex "$git daemon 8080"
18 | }
19 |
20 | function CloneFrom8080()
21 | {
22 | $git = "C:\src\KBGit\bin\Debug\netcoreapp2.0\win10-x64\KBGit.exe"
23 | iex "$git clone http://localhost 8080 master"
24 | }
--------------------------------------------------------------------------------
/KBGit.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp2.0
6 | KbgSoft.KBGit
7 | KbgSoft.KBGit.Program
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/kbgit.tests/kbgit.tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/kbgit.tests/ReadmeHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 | using System.Text.RegularExpressions;
5 | using TeamBinary.LineCounter;
6 | using Xunit;
7 |
8 | namespace kbgit.tests
9 | {
10 | public class ReadMeHelper
11 | {
12 | [Fact]
13 | public void MutateReadme()
14 | {
15 | var basePath = Path.Combine(Assembly.GetExecutingAssembly().Location, "..", "..", "..", "..", "..");
16 |
17 | var stats = new DirWalker().DoWork(new[] {Path.Combine(basePath, "Git.cs")});
18 |
19 | var shieldsRegEx = new Regex(".*", RegexOptions.Singleline);
20 | var githubShields = new WebFormatter().CreateGithubShields(stats);
21 |
22 | var readmePath = Path.Combine(basePath, "README.md");
23 | var oldReadme = File.ReadAllText(readmePath);
24 | var newReadMe = shieldsRegEx.Replace(oldReadme, $"\r\n{githubShields}\r\n");
25 |
26 | if (oldReadme != newReadMe)
27 | File.WriteAllText(readmePath, newReadMe);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/kbgit.tests/RepoBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using KbgSoft.KBGit;
4 |
5 | namespace kbgit.tests
6 | {
7 | public class RepoBuilder
8 | {
9 | readonly string basePath;
10 | public KBGit Git;
11 |
12 | public RepoBuilder() : this(@"c:\temp\", Guid.NewGuid()) { }
13 |
14 | public RepoBuilder(Guid unittestguid) : this(@"c:\temp\", unittestguid) { }
15 |
16 | public RepoBuilder(string basePath) : this(basePath, Guid.NewGuid()) { }
17 |
18 | public RepoBuilder(string basePath, Guid unittestguid)
19 | {
20 | this.basePath = Path.Combine(basePath, $"kbgit\\{unittestguid}");
21 | Directory.CreateDirectory(this.basePath);
22 | }
23 |
24 | public KBGit BuildEmptyRepo()
25 | {
26 | Git = new KBGit(basePath);
27 | Git.InitializeRepository();
28 | return Git;
29 | }
30 |
31 | public RepoBuilder EmptyRepo()
32 | {
33 | Git = new KBGit(basePath);
34 | Git.InitializeRepository();
35 | return this;
36 | }
37 |
38 | public KBGit Build2Files3Commits()
39 | {
40 | Git = BuildEmptyRepo();
41 |
42 | AddFile("a.txt", "aaaaa");
43 | Git.Commit("Add a", "kasper", new DateTime(2017,1,1,1,1,1));
44 |
45 | AddFile("b.txt", "bbbb");
46 | Git.Commit("Add b", "kasper", new DateTime(2017, 2, 2, 2, 2, 2));
47 |
48 | AddFile("a.txt", "v2av2av2av2a");
49 | Git.Commit("Add a2", "kasper", new DateTime(2017, 3, 3, 3, 3, 3));
50 |
51 | return Git;
52 | }
53 |
54 | public RepoBuilder AddFile(string path) => AddFile(path, Guid.NewGuid().ToString());
55 |
56 | public RepoBuilder AddFile(string path, string content)
57 | {
58 | var filepath = Path.Combine(Git.CodeFolder, path);
59 | new FileInfo(filepath).Directory.Create();
60 |
61 | File.WriteAllText(filepath, content);
62 | return this;
63 | }
64 |
65 | public string ReadFile(string path)
66 | {
67 | return File.ReadAllText(Path.Combine(Git.CodeFolder, path));
68 | }
69 |
70 | public RepoBuilder DeleteFile(string path)
71 | {
72 | File.Delete(Path.Combine(basePath, path));
73 | return this;
74 | }
75 |
76 | public Id Commit() => Commit("Some message");
77 |
78 | public Id Commit(string message)
79 | {
80 | return Git.Commit(message, "author", DateTime.Now);
81 | }
82 |
83 | public RepoBuilder NewBranch(string branch)
84 | {
85 | Git.Branches.CreateBranch(branch);
86 | return this;
87 | }
88 |
89 | public RepoBuilder AddLocalHostRemote(int port)
90 | {
91 | Git.Hd.Remotes.Add(new Remote()
92 | {
93 | Name = "origin",
94 | Url = new Uri($"http://localhost:{port}")
95 | });
96 | return this;
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KBGit - Git implemented from scratch in 500 lines of code (or less ...)
2 |
3 | Project statistics:
4 | []()
5 | []()
6 |
7 |
8 | Recently I dug into their implementation details of Git. The simplicity of the implementation was completely mind-boggling.
9 | What a gem I found! Such a simple model for something conceptually complex as a source code versioning system. I almost couldn't believe it!
10 |
11 | I wanted to share the beauty of the details of Git's inner workings. So how to best do this?
12 | I found inspiration from Terry A. Davis' work on [TempleOS](http://www.templeos.org)
13 |
14 | > Everybody is obsessed, Jedi mind-tricked, by the notion that when you scale-up,
15 | > it doesn't get bad, it gets worse. They automatically think things are going to
16 | > get bigger. Guess what happens when you scale down? It doesn't get good, it
17 | > gets better!
18 | > -- Terry A. Davis (https://templeos.sheikhs.space/Wb/Doc/Strategy.html)
19 |
20 | So why would you want to grok this code? The reward reaped is that you'll find many of the operations of Git much more natural. They will suddenly make sense.
21 | In order to have a fighting chance of conveying anything to any reader (and as Terry says). Less is more. **I challenged myself to keep the implementation
22 | to a maximum of 500 lines of code.. for a "complete re-implementation of git"!**
23 |
24 | I want a fair game, though. And 500 lines of code is not a lot. To prevent myself from spiraling into an code-obfuscation contest in order to save
25 | a few lines of code, I'm counting lines using a [simple line counting library](https://github.com/kbilsted/LineCounter.Net)
26 | which count only semantic lines (i.e. exclude empty lines, lines only containing `{`, `}`,...).
27 |
28 | KBGit..what?? My initials are *K.B.G.* - hence the name KBGit :-)
29 |
30 | ## Features implemented
31 |
32 | * commits
33 | * branches
34 | * (create, list, delete)
35 | * detached heads
36 | * HEAD branch
37 | * checkout branches or commits
38 | * logging
39 | * push + pull
40 | * clone
41 | * remote (create, list, delete)
42 | * command line parser
43 | * store git state on disk
44 |
45 |
46 | ## Planned work
47 |
48 | * git log
49 | * graphical (--graph)
50 | * patch (-p)
51 | * "less" implementation
52 | * git INDEX rather than scanning files
53 | * blob compression
54 | * diff'er to show changes to files
55 | * and select changes to index...
56 |
57 |
58 | I will blog about the implementation on [http://firstclassthoughts.co.uk/](http://firstclassthoughts.co.uk/)
59 | when the implementation has stabilized. Comments etc. are much welcommed.
60 |
61 | For now the reading guide is to just read the `Git.cs` file. The main classes are the `CommandLineHandling` which is the facade of the implementation.
62 | The first class `KBGit` holds or redirects the main functionalities of Git. For now, just browse around with outset in those classes.
63 |
64 | Articles
65 | * Command line parsing: [http://firstclassthoughts.co.uk/Articles/Readability/PreferDeclarativeCodeOverImperativeCode.html](http://firstclassthoughts.co.uk/Articles/Readability/PreferDeclarativeCodeOverImperativeCode.html)
66 |
--------------------------------------------------------------------------------
/KBGit.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27130.2010
5 | MinimumVisualStudioVersion = 15.0.26124.0
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KBGit", "KBGit.csproj", "{78F37C52-22F6-443F-845F-A66DBFEA3E12}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kbgit.tests", "kbgit.tests\kbgit.tests.csproj", "{2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8203C57F-641A-4F59-AC11-541A351231A3}"
11 | ProjectSection(SolutionItems) = preProject
12 | kbgithelper.ps1 = kbgithelper.ps1
13 | EndProjectSection
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Debug|x64 = Debug|x64
19 | Debug|x86 = Debug|x86
20 | Release|Any CPU = Release|Any CPU
21 | Release|x64 = Release|x64
22 | Release|x86 = Release|x86
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Debug|x64.ActiveCfg = Debug|Any CPU
28 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Debug|x64.Build.0 = Debug|Any CPU
29 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Debug|x86.ActiveCfg = Debug|Any CPU
30 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Debug|x86.Build.0 = Debug|Any CPU
31 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Release|x64.ActiveCfg = Release|Any CPU
34 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Release|x64.Build.0 = Release|Any CPU
35 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Release|x86.ActiveCfg = Release|Any CPU
36 | {78F37C52-22F6-443F-845F-A66DBFEA3E12}.Release|x86.Build.0 = Release|Any CPU
37 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Debug|x64.ActiveCfg = Debug|Any CPU
40 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Debug|x64.Build.0 = Debug|Any CPU
41 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Debug|x86.ActiveCfg = Debug|Any CPU
42 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Debug|x86.Build.0 = Debug|Any CPU
43 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Release|x64.ActiveCfg = Release|Any CPU
46 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Release|x64.Build.0 = Release|Any CPU
47 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Release|x86.ActiveCfg = Release|Any CPU
48 | {2CDBAE6B-FB63-4AB4-9E5B-357B8D9E0921}.Release|x86.Build.0 = Release|Any CPU
49 | EndGlobalSection
50 | GlobalSection(SolutionProperties) = preSolution
51 | HideSolutionNode = FALSE
52 | EndGlobalSection
53 | GlobalSection(ExtensibilityGlobals) = postSolution
54 | SolutionGuid = {B9B486F5-9119-4800-95E0-2541B2740209}
55 | EndGlobalSection
56 | EndGlobal
57 |
--------------------------------------------------------------------------------
/.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 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/kbgit.tests/KBGitTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using KbgSoft.KBGit;
7 | using Xunit;
8 |
9 | namespace kbgit.tests
10 | {
11 | public class KbGitTests
12 | {
13 | private RepoBuilder repoBuilder = new RepoBuilder();
14 |
15 | [Fact]
16 | public void When_committing_Then_move_branch_pointer()
17 | {
18 | var id1 = repoBuilder.EmptyRepo().AddFile("a.txt").Commit();
19 |
20 | var branchTip = repoBuilder.Git.Hd.Branches.Single().Value.Tip;
21 | Assert.Equal(id1, branchTip);
22 | }
23 |
24 | [Fact]
25 | public void When_committing_Then_set_parent_to_previous_head()
26 | {
27 | var parentId = repoBuilder.EmptyRepo().AddFile("a.txt").Commit();
28 |
29 | var commitId = repoBuilder.AddFile("a.txt").Commit();
30 |
31 | Assert.Equal(parentId, repoBuilder.Git.Hd.Commits[commitId].Parents.First());
32 | }
33 |
34 | [Fact]
35 | public void CommitWhenHeadless()
36 | {
37 | repoBuilder = new RepoBuilder(@"c:\temp\");
38 | var git = repoBuilder.Build2Files3Commits();
39 | git.Branches.Checkout(git.HeadRef(1));
40 | repoBuilder.AddFile("newfile", "dslfk");
41 |
42 | var id = git.Commit("headless commit", "a", new DateTime(2010, 11, 12));
43 |
44 | Assert.Equal("f4982f442bf946c3678bc68761a1da953ff1f61020311d1802167288b5514087", id.ToString());
45 | }
46 |
47 | [Fact]
48 | public void When_Commit_a_similar_situation_to_a_previous_commit_Then_should_not_incread_blobs_nor_tree_blobs()
49 | {
50 | repoBuilder
51 | .EmptyRepo()
52 | .AddFile("a.txt")
53 | .Commit();
54 |
55 | repoBuilder
56 | .AddFile("b.txt")
57 | .Commit();
58 |
59 | Assert.Equal(2, repoBuilder.Git.Hd.Blobs.Count);
60 | Assert.Equal(2, repoBuilder.Git.Hd.Commits.Count);
61 |
62 | repoBuilder
63 | .DeleteFile("b.txt")
64 | .Commit("deleted b");
65 |
66 | Assert.Equal(2, repoBuilder.Git.Hd.Blobs.Count);
67 | Assert.Equal(3, repoBuilder.Git.Hd.Commits.Count);
68 | }
69 |
70 | public string FileSystemScanFolder(KBGit git)
71 | {
72 | return git.FileSystemScanFolder(git.CodeFolder).ToString();
73 | }
74 |
75 | [Fact]
76 | public void Given_two_toplevel_files_Then_()
77 | {
78 | repoBuilder = new RepoBuilder(@"c:\temp\");
79 | var git = repoBuilder.BuildEmptyRepo();
80 | repoBuilder.AddFile("car.txt", "car");
81 | repoBuilder.AddFile("door.txt", "door");
82 |
83 | var files = FileSystemScanFolder(git);
84 |
85 | Assert.Equal(@"tree 2
86 | blob car.txt
87 | blob door.txt", files);
88 | }
89 |
90 | [Fact]
91 | public void Given_two_files_in_subfolder_Then_()
92 | {
93 | repoBuilder = new RepoBuilder(@"c:\temp\");
94 | var git = repoBuilder.BuildEmptyRepo();
95 | repoBuilder.AddFile(@"FeatureVolvo\car.txt", "car");
96 | repoBuilder.AddFile(@"FeatureVolvo\door.txt", "door");
97 |
98 | var files = FileSystemScanFolder(git);
99 |
100 | Assert.Equal(
101 | @"tree 1
102 | tree 2 FeatureVolvo\
103 | blob FeatureVolvo\car.txt
104 | blob FeatureVolvo\door.txt", files);
105 | }
106 |
107 | [Fact]
108 | public void Get_folders_and_files()
109 | {
110 | repoBuilder = new RepoBuilder(@"c:\temp\");
111 | var git = repoBuilder.BuildEmptyRepo();
112 | repoBuilder.AddFile(@"FeatureVolvo\car.txt", "car");
113 | repoBuilder.AddFile(@"FeatureGarden\tree.txt", "tree");
114 | repoBuilder.AddFile(@"FeatureGarden\shovel.txt", "shovel");
115 | repoBuilder.AddFile(@"FeatureGarden\Suburb\grass.txt", "grass");
116 | repoBuilder.AddFile(@"FeatureGarden\Suburb\mover.txt", "mover");
117 |
118 | var files = FileSystemScanFolder(git);
119 |
120 | Assert.Equal(
121 | @"tree 2
122 | tree 3 FeatureGarden\
123 | blob FeatureGarden\shovel.txt
124 | blob FeatureGarden\tree.txt
125 | tree 2 FeatureGarden\Suburb\
126 | blob FeatureGarden\Suburb\grass.txt
127 | blob FeatureGarden\Suburb\mover.txt
128 | tree 1 FeatureVolvo\
129 | blob FeatureVolvo\car.txt"
130 | , files);
131 | }
132 |
133 | [Fact]
134 | public void Visit()
135 | {
136 | var repoBuilder = new RepoBuilder(@"c:\temp\");
137 | var git = repoBuilder.BuildEmptyRepo();
138 | repoBuilder.AddFile(@"FeatureVolvo\car.txt", "car");
139 | repoBuilder.AddFile(@"FeatureGarden\tree.txt", "tree");
140 | repoBuilder.AddFile(@"FeatureGarden\shovel.txt", "shovel");
141 | repoBuilder.AddFile(@"FeatureGarden\Suburb\grass.txt", "grass");
142 | string buf = "";
143 | git.FileSystemScanFolder(git.CodeFolder).Visit(x =>
144 | {
145 | if (x is TreeTreeLine t)
146 | buf += $"visittree {t.Path}\r\n";
147 | if (x is BlobTreeLine b)
148 | buf += $"visitblob {b.Path}\r\n";
149 | });
150 |
151 | Assert.Equal(@"visittree
152 | visittree FeatureGarden\
153 | visitblob FeatureGarden\shovel.txt
154 | visitblob FeatureGarden\tree.txt
155 | visittree FeatureGarden\Suburb\
156 | visitblob FeatureGarden\Suburb\grass.txt
157 | visittree FeatureVolvo\
158 | visitblob FeatureVolvo\car.txt
159 | ", buf);
160 | }
161 |
162 | [Fact]
163 | public void Ensure_internal_git_file_is_skipped()
164 | {
165 | var git = repoBuilder.EmptyRepo().AddFile("babe.txt").Git;
166 | git.StoreState();
167 |
168 | var files = FileSystemScanFolder(git);
169 |
170 | Assert.Equal(@"tree 1
171 | blob babe.txt", files);
172 | }
173 | }
174 |
175 | public class KBGitHeadTests
176 | {
177 | RepoBuilder repoBuilder = new RepoBuilder();
178 |
179 | [Fact]
180 | public void Given_fresh_repo_When_getting_headinfo_Then_fail()
181 | {
182 | var git = new RepoBuilder(@"c:\temp\").BuildEmptyRepo();
183 |
184 | Assert.Null(git.Hd.Head.GetId(git.Hd));
185 | Assert.Equal("master", git.Hd.Head.Branch);
186 | }
187 |
188 | [Fact]
189 | public void Given_repo_When_committing_getting_headinfo_Then_return_info()
190 | {
191 | repoBuilder = new RepoBuilder(@"c:\temp\");
192 | var firstId = repoBuilder.EmptyRepo().AddFile("a.txt").Commit();
193 |
194 | Assert.Equal(firstId, repoBuilder.Git.Hd.Head.GetId(repoBuilder.Git.Hd));
195 | Assert.Null(repoBuilder.Git.Hd.Head.Id);
196 | Assert.Equal("master", repoBuilder.Git.Hd.Head.Branch);
197 | }
198 |
199 | [Fact]
200 | public void Given_repo_When_getting_HeadRef_1_Then_return_parent_of_HEAD()
201 | {
202 | var git = repoBuilder.Build2Files3Commits();
203 | var parentOfHead = git.Hd.Commits[git.Hd.Head.GetId(git.Hd)].Parents.First();
204 |
205 | Assert.Equal(parentOfHead, git.HeadRef(1));
206 | }
207 |
208 | [Fact]
209 | public void When_detached_head_and_commit_move_Then_update_head()
210 | {
211 | var detachedId = repoBuilder
212 | .EmptyRepo()
213 | .AddFile("a.txt")
214 | .Commit();
215 | repoBuilder
216 | .AddFile("b.txt")
217 | .Commit();
218 | repoBuilder.Git.Branches.Checkout(detachedId);
219 | var detachedId2=repoBuilder
220 | .AddFile("a.txt")
221 | .Commit();
222 |
223 | Assert.Null(repoBuilder.Git.Hd.Head.Branch);
224 | Assert.Equal(detachedId2, repoBuilder.Git.Hd.Head.Id);
225 | Assert.Equal(detachedId2, repoBuilder.Git.Hd.Head.GetId(repoBuilder.Git.Hd));
226 | }
227 | }
228 |
229 | public class IdTests
230 | {
231 | [Fact]
232 | public void When_equal_Then_equals_returns_true()
233 | {
234 | var id1 = new Id("162b60c2809016e893b96d2d941c0c68ba6d2ac25cbc12b21d5678656fca8c8f");
235 | var id2 = new Id("162b60c2809016e893b96d2d941c0c68ba6d2ac25cbc12b21d5678656fca8c8f");
236 |
237 | Assert.True(id1.Equals(id2));
238 | }
239 |
240 | [Fact]
241 | public void When_not_equal_Then_equals_returns_false()
242 | {
243 | var id1 = new Id("162b60c2809016e893b96d2d941c0c68ba6d2ac25cbc12b21d5678656fca8c8f");
244 | var id2 = new Id("612b60c2809016e893b96d2d941c0c68ba6d2ac25cbc12b21d5678656fca8c8f");
245 |
246 | Assert.False(id1.Equals(id2));
247 | }
248 | }
249 |
250 | public class LogTests
251 | {
252 | [Fact]
253 | public void When_empty_repo_Then_log_returns_empty()
254 | {
255 | var repoBuilder = new RepoBuilder().EmptyRepo();
256 |
257 | Assert.Equal(@"
258 | Log for master
259 | ", repoBuilder.Git.Log());
260 | }
261 |
262 | [Fact]
263 | public void When_one_commit_Then_log_one_line()
264 | {
265 | var repoBuilder = new RepoBuilder(new Guid("6c7f821e-5cb2-45de-9365-3e35887c0ee6"))
266 | .EmptyRepo()
267 | .AddFile("a.txt", "some content");
268 |
269 | repoBuilder.Git.Commit("Add a.txt", "kasper graversen", new DateTime(2018, 3, 1, 12, 22, 33));
270 |
271 | Assert.Equal(@"
272 | Log for master
273 | * 06cd57d8d2feececc9eb48adda4cea5b57482324267f1e9632c16079ac6d793e - Add a.txt (2018/03/01 12:22:33)
274 | ", repoBuilder.Git.Log());
275 | }
276 |
277 | [Fact]
278 | public void When_two_commits_Then_log_twoline()
279 | {
280 | var repoBuilder = new RepoBuilder(new Guid("b3b12f1c-f455-4987-b2d7-5db08d9e1ee4"))
281 | .EmptyRepo()
282 | .AddFile("a.txt", "some content");
283 | repoBuilder.Git.Commit("Add a.txt", "kasper graversen", new DateTime(2018, 3, 1, 12, 22, 33));
284 | repoBuilder.AddFile("a.txt", "changed a...");
285 | repoBuilder.Git.Commit("Changed a.txt", "kasper graversen", new DateTime(2018, 3, 2, 13, 24, 34));
286 |
287 | var actual = repoBuilder.Git.Log();
288 | Assert.Equal(@"
289 | Log for master
290 | * dd3044753fdb212c9248da29005a6d4765e3bbe302efff96a9321bf8ea710b83 - Changed a.txt (2018/03/02 01:24:34)
291 | * 06cd57d8d2feececc9eb48adda4cea5b57482324267f1e9632c16079ac6d793e - Add a.txt (2018/03/01 12:22:33)
292 | ", actual);
293 | }
294 |
295 | [Fact]
296 | public void When_two_commits_on_master_and_one_on_feature_Then_log_both_branches()
297 | {
298 | var repoBuilder = new RepoBuilder(new Guid("186d2ac8-1e9c-4e86-b1ac-b18208adead4"))
299 | .EmptyRepo()
300 | .AddFile("a.txt", "some content");
301 | repoBuilder.Git.Commit("Add a.txt", "kasper graversen", new DateTime(2018, 3, 1, 12, 22, 33));
302 | repoBuilder.AddFile("a.txt", "changed a...");
303 | repoBuilder.Git.Commit("Changed a.txt", "kasper graversen", new DateTime(2018, 3, 2, 13, 24, 34));
304 | repoBuilder
305 | .NewBranch("feature/speed")
306 | .AddFile("a.txt", "speedup!")
307 | .Git.Commit("Speedup a.txt", "kasper graversen", new DateTime(2018, 4, 3, 15, 26, 37));
308 |
309 | var actual = repoBuilder.Git.Log();
310 | Assert.Equal(@"
311 | Log for master
312 | * dd3044753fdb212c9248da29005a6d4765e3bbe302efff96a9321bf8ea710b83 - Changed a.txt (2018/03/02 01:24:34)
313 | * 06cd57d8d2feececc9eb48adda4cea5b57482324267f1e9632c16079ac6d793e - Add a.txt (2018/03/01 12:22:33)
314 |
315 | Log for feature/speed
316 | * fafcd20734eda4c9849aea8cb831c87f225909e32686637c54d3896513ecfca0 - Speedup a.txt (2018/04/03 03:26:37)
317 | ", actual);
318 | }
319 | }
320 |
321 | public class GitCommitTests
322 | {
323 | RepoBuilder repoBuilder = new RepoBuilder();
324 |
325 | [Fact]
326 | public void When_Commit_Then_treeid_information_is_Stored()
327 | {
328 | repoBuilder.EmptyRepo().AddFile("a.txt").Commit();
329 |
330 | var commit = repoBuilder.Git.Hd.Commits.First();
331 | Assert.NotNull(commit.Value.Tree);
332 | Assert.NotNull(commit.Value.TreeId);
333 | }
334 |
335 | [Fact]
336 | public void When_Commit_Then_content_is_stored()
337 | {
338 | var filename = "a.txt";
339 | var id1 = repoBuilder
340 | .EmptyRepo()
341 | .AddFile(filename, "version 1 a")
342 | .Commit();
343 | var id2 = repoBuilder
344 | .AddFile(filename, "version 2 a")
345 | .Commit();
346 |
347 | repoBuilder.Git.Branches.Checkout(id1);
348 | Assert.Equal("version 1 a", repoBuilder.ReadFile(filename));
349 |
350 | repoBuilder.Git.Branches.Checkout(id2);
351 | Assert.Equal("version 2 a", repoBuilder.ReadFile(filename));
352 | }
353 |
354 | [Fact]
355 | public void When_branching_and_commit_and_update_back_Then_reset_content_to_old_branch()
356 | {
357 | repoBuilder
358 | .EmptyRepo()
359 | .AddFile("a.txt", "version 1 a")
360 | .Commit();
361 | repoBuilder.NewBranch("featurebranch")
362 | .AddFile("b.txt", "version 1 b")
363 | .Commit();
364 | IEnumerable FilesInRepo() => repoBuilder.Git
365 | .FileSystemScanSubFolder(repoBuilder.Git.CodeFolder)
366 | .OfType()
367 | .Select(x => x.Path);
368 |
369 | Assert.Equal(new[] {"a.txt", "b.txt"}, FilesInRepo());
370 |
371 | repoBuilder.Git.Branches.Checkout("master");
372 | Assert.Equal(new[] { "a.txt" }, FilesInRepo());
373 |
374 | repoBuilder.Git.Branches.Checkout("featurebranch");
375 | Assert.Equal(new[] { "a.txt", "b.txt" }, FilesInRepo());
376 | }
377 |
378 | [Fact]
379 | public void When_detached_head_Then_git_branches_shows_detached_as_branch()
380 | {
381 | repoBuilder = new RepoBuilder(@"c:\temp\");
382 | var detachedId = repoBuilder
383 | .EmptyRepo()
384 | .AddFile("a.txt")
385 | .Commit();
386 | repoBuilder
387 | .AddFile("b.txt")
388 | .Commit();
389 |
390 | repoBuilder.Git.Branches.Checkout(detachedId);
391 |
392 | Assert.Equal($@"* (HEAD detached at {detachedId.ToString().Substring(0, 7)})
393 | master", repoBuilder.Git.Branches.ListBranches());
394 | }
395 |
396 | [Fact]
397 | public void When_branching_Then_Branchinfo_show_new_branchname()
398 | {
399 | repoBuilder = new RepoBuilder(@"c:\temp\");
400 | repoBuilder
401 | .EmptyRepo()
402 | .AddFile("a.txt")
403 | .Commit();
404 | Assert.Equal("* master", repoBuilder.Git.Branches.ListBranches());
405 |
406 | repoBuilder.NewBranch("featurebranch");
407 | Assert.Equal(@"* featurebranch
408 | master", repoBuilder.Git.Branches.ListBranches());
409 | }
410 |
411 | [Fact]
412 | public void When_deleting_branch_Then_delete_branch()
413 | {
414 | repoBuilder
415 | .EmptyRepo()
416 | .AddFile("a.txt")
417 | .Commit();
418 | repoBuilder.NewBranch("featurebranch");
419 | repoBuilder.Git.Branches.Checkout("master");
420 |
421 | var msg = repoBuilder.Git.Branches.DeleteBranch("featurebranch");
422 |
423 | Assert.StartsWith("Deleted branch featurebranch (was ", msg);
424 | Assert.Equal(@"* master", repoBuilder.Git.Branches.ListBranches());
425 | }
426 |
427 | [Fact]
428 | public void When_deleting_current_branch_Then_fail()
429 | {
430 | repoBuilder
431 | .EmptyRepo()
432 | .AddFile("a.txt")
433 | .Commit();
434 | repoBuilder.NewBranch("featurebranch");
435 |
436 | Assert.Throws(() => repoBuilder.Git.Branches.DeleteBranch("featurebranch"));
437 | }
438 | }
439 |
440 | public class NetworkingTests
441 | {
442 | [Fact]
443 | public void When_pulling_Then_receive_all_nodes()
444 | {
445 | var remoteGit = new RepoBuilder().Build2Files3Commits();
446 | var gitServer = SpinUpServer(remoteGit, 18081);
447 | var localGit = new RepoBuilder().EmptyRepo().AddLocalHostRemote(18081).Git;
448 |
449 | new GitNetworkClient().PullBranch(localGit.Hd.Remotes.First(), "master", localGit);
450 |
451 | var actual = localGit.Log();
452 | Assert.Equal(@"
453 | Log for master
454 |
455 | Log for origin/master
456 | * e7ea1966e7cb9b96e956a53d4a7042aa4dcc69720363dd928087af50a8c26b32 - Add a2 (2017/03/03 03:03:03)
457 | * ed0ea7ea22cbaf8b34ee711974568d42853aff967fdb8c21fac93788d8e8e954 - Add b (2017/02/02 02:02:02)
458 | * f0800442b12313bbac440b9ae0aef5b2c1978c95e8ccaf4197d6816bd29bf673 - Add a (2017/01/01 01:01:01)
459 | ", actual);
460 | gitServer.Abort();
461 | }
462 |
463 | [Fact]
464 | public void When_pushing_Then_push_nodes_and_update_branchpointer_on_server()
465 | {
466 | var remoteGit = new RepoBuilder().BuildEmptyRepo();
467 | var gitServer = SpinUpServer(remoteGit, 18083);
468 | var localbuilder = new RepoBuilder();
469 | var localGit = localbuilder.Build2Files3Commits();
470 | localbuilder.AddLocalHostRemote(18083);
471 |
472 | Branch branch = localGit.Hd.Branches["master"];
473 | var commits = localGit.GetReachableNodes(branch.Tip).ToArray();
474 | new GitNetworkClient().PushBranch(localGit.Hd.Remotes.First(), "master", branch, null, commits);
475 |
476 | var actual = remoteGit.Log();
477 | Assert.Equal(@"
478 | Log for master
479 | * e7ea1966e7cb9b96e956a53d4a7042aa4dcc69720363dd928087af50a8c26b32 - Add a2 (2017/03/03 03:03:03)
480 | * ed0ea7ea22cbaf8b34ee711974568d42853aff967fdb8c21fac93788d8e8e954 - Add b (2017/02/02 02:02:02)
481 | * f0800442b12313bbac440b9ae0aef5b2c1978c95e8ccaf4197d6816bd29bf673 - Add a (2017/01/01 01:01:01)
482 | ", actual);
483 | gitServer.Abort();
484 | }
485 |
486 | private Task t;
487 | GitServer SpinUpServer(KBGit git, int port)
488 | {
489 | var server = new GitServer(git);
490 | t = new TaskFactory().StartNew(() => server.StartDaemon(port));
491 |
492 | while (!server.Running.HasValue)
493 | Thread.Sleep(50);
494 |
495 | return server;
496 | }
497 | }
498 |
499 | public class CommandHandlingTests
500 | {
501 | bool logWasMatched = false;
502 | bool commitWasMatched = false;
503 | readonly RepoBuilder repoBuilder = new RepoBuilder();
504 |
505 | GrammarLine[] GetTestConfigurationStub() => new[]
506 | {
507 | new GrammarLine("make log", new[] {"log"}, (git, args) => { logWasMatched = true; }),
508 | new GrammarLine("make commit", new[] {"commit", ""}, (git, args) => { commitWasMatched = true; }),
509 | };
510 |
511 | [Fact]
512 | public void When_printhelp_Then_all_commands_are_explained()
513 | {
514 | var helpText = new CommandLineHandling().Handle(null, CommandLineHandling.Config, new[]{"unmatched","parameters"});
515 |
516 | Assert.Equal(
517 | @"KBGit Help
518 | ----------
519 | git init - Initialize an empty repo.
520 | git commit -m - Make a commit.
521 | git log - Show the commit log.
522 | git checkout -b - Create a new new branch at HEAD.
523 | git checkout -b - Create a new new branch at commit id.
524 | git checkout - Update HEAD.
525 | git branch -D - Delete a branch.
526 | git branch - List existing branches.
527 | git gc - Garbage collect.
528 | git daemon - Start git as a server.
529 | git pull - Pull code.
530 | git push - Push code.
531 | git clone - Clone code from other server.
532 | git remote -v - List remotes.
533 | git remote add - Add remote.
534 | git remote rm - Remove remote.", helpText);
535 | }
536 |
537 | [Fact]
538 | public void When_calling_with_specific_arguments_Then_match()
539 | {
540 | new CommandLineHandling().Handle(repoBuilder.EmptyRepo().Git, GetTestConfigurationStub(), new[] { "log" });
541 |
542 | Assert.True(logWasMatched);
543 | Assert.False(commitWasMatched);
544 | }
545 |
546 | [Fact]
547 | public void When_not_calling_with_unrecognized_arguments_Then_not_match()
548 | {
549 | new CommandLineHandling().Handle(repoBuilder.EmptyRepo().Git, GetTestConfigurationStub(), new[] {"NOTLOG"});
550 |
551 | Assert.False(logWasMatched);
552 | Assert.False(commitWasMatched);
553 | }
554 |
555 | [Fact]
556 | public void When_calling_with_too_few_arguments_Then_not_match()
557 | {
558 | new CommandLineHandling().Handle(repoBuilder.EmptyRepo().Git, GetTestConfigurationStub(), new[] { "git" });
559 |
560 | Assert.False(logWasMatched);
561 | Assert.False(commitWasMatched);
562 | }
563 |
564 | [Fact]
565 | public void When_calling_with_too_many_arguments_Then_not_match()
566 | {
567 | new CommandLineHandling().Handle(repoBuilder.EmptyRepo().Git, GetTestConfigurationStub(), new[] { "log", "too", "many", "args" });
568 |
569 | Assert.False(logWasMatched);
570 | Assert.False(commitWasMatched);
571 | }
572 |
573 | [Fact]
574 | public void When_calling_with_specific_argumenthole_Then_match()
575 | {
576 | new CommandLineHandling().Handle(repoBuilder.EmptyRepo().Git, GetTestConfigurationStub(), new[] { "commit", "some message"});
577 |
578 | Assert.False(logWasMatched);
579 | Assert.True(commitWasMatched);
580 | }
581 |
582 | [Fact]
583 | public void When_not_calling_with_specific_argumenthole_Then_not_match()
584 | {
585 | new CommandLineHandling().Handle(repoBuilder.EmptyRepo().Git, GetTestConfigurationStub(), new[] { "commit" });
586 |
587 | Assert.False(logWasMatched);
588 | Assert.False(commitWasMatched);
589 | }
590 | }
591 |
592 | public class RemotesTest
593 | {
594 | private RepoBuilder repoBuilder = new RepoBuilder();
595 |
596 | [Fact]
597 | public void Given_no_remotes_When_listing_Then_return_empty()
598 | {
599 | Assert.Equal("", repoBuilder.BuildEmptyRepo().Remotes.List());
600 | }
601 |
602 | [Fact]
603 | public void When_adding_remotes_Then_listing_shows_them()
604 | {
605 | var git = repoBuilder.BuildEmptyRepo();
606 | git.Remotes.Remotes.Add(new Remote(){Name = "origin", Url = new Uri("https://kbgit.world:8080")});
607 | git.Remotes.Remotes.Add(new Remote(){Name = "ghulu", Url = new Uri("https://Ghu.lu:8080")});
608 |
609 | Assert.Equal(
610 | @"origin https://kbgit.world:8080/
611 | ghulu https://ghu.lu:8080/", git.Remotes.List());
612 | }
613 |
614 | [Fact]
615 | public void When_removing_remotes_Then_listing_does_not_show_them()
616 | {
617 | var git = repoBuilder.BuildEmptyRepo();
618 | git.Remotes.Remotes.Add(new Remote() { Name = "origin", Url = new Uri("https://kbgit.world:8080") });
619 | git.Remotes.Remotes.Add(new Remote() { Name = "ghulu", Url = new Uri("https://Ghu.lu:8080") });
620 | git.Remotes.Remove("origin");
621 |
622 | Assert.Equal(@"ghulu https://ghu.lu:8080/", git.Remotes.List());
623 | }
624 | }
625 | }
626 |
--------------------------------------------------------------------------------
/Git.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Net.Http;
7 | using System.Runtime.Serialization.Formatters.Binary;
8 | using System.Security.Cryptography;
9 | using System.Text;
10 |
11 | namespace KbgSoft.KBGit
12 | {
13 | public class GrammarLine
14 | {
15 | public readonly string Explanation;
16 | public readonly string[] Grammar;
17 | public readonly Func ActionOnMatch;
18 |
19 | public GrammarLine(string explanation, string[] grammar, Action actionOnMatch) : this(explanation, grammar, (git, arg) => { actionOnMatch(git, arg); return null; })
20 | { }
21 |
22 | public GrammarLine(string explanation, string[] grammar, Func actionOnMatch)
23 | {
24 | Explanation = explanation; Grammar = grammar; ActionOnMatch = actionOnMatch;
25 | }
26 | }
27 |
28 | public class CommandLineHandling
29 | {
30 | public static readonly GrammarLine[] Config =
31 | {
32 | new GrammarLine("Initialize an empty repo", new[] { "init"}, (git, args) => git.InitializeRepository()),
33 | new GrammarLine("Make a commit", new[] { "commit", "-m", ""}, (git, args) => { git.Commit(args[2], "author", DateTime.Now); }),
34 | new GrammarLine("Show the commit log", new[] { "log"}, (git, args) => git.Log()),
35 | new GrammarLine("Create a new new branch at HEAD", new[] { "checkout", "-b", ""}, (git, args) => git.Branches.CreateBranch(args[2])),
36 | new GrammarLine("Create a new new branch at commit id", new[] { "checkout", "-b", "", ""}, (git, args) => git.Branches.CreateBranch(args[2], new Id(args[3]))),
37 | new GrammarLine("Update HEAD", new[] { "checkout", ""}, (git, args) => git.Hd.Branches.ContainsKey(args[1]) ? git.Branches.Checkout(args[1]) : git.Branches.Checkout(new Id(args[1]))),
38 | new GrammarLine("Delete a branch", new[] { "branch", "-D", ""}, (git, args) => git.Branches.DeleteBranch(args[2])),
39 | new GrammarLine("List existing branches", new[] { "branch"}, (git, args) => git.Branches.ListBranches()),
40 | new GrammarLine("Garbage collect", new[] { "gc" }, (git, args) => { git.Gc(); }),
41 | new GrammarLine("Start git as a server", new[] { "daemon", "" }, (git, args) => { new GitServer(git).StartDaemon(int.Parse(args[1])); }),
42 | new GrammarLine("Pull code", new[] { "pull", "", ""}, (git, args) => { new GitNetworkClient().PullBranch(git.Hd.Remotes.First(x => x.Name == args[1]), args[2], git);}),
43 | new GrammarLine("Push code", new[] { "push", "", ""}, (git, args) => { new GitNetworkClient().PushBranch(git.Hd.Remotes.First(x => x.Name == args[1]), args[2], git.Hd.Branches[args[2]], null, git.GetReachableNodes(git.Hd.Branches[args[2]].Tip).ToArray()); }),
44 | new GrammarLine("Clone code from other server", new[] { "clone", "", ""}, (git, args) => { new GitNetworkClient().CloneBranch(git, "origin", args[1], args[2]); }),
45 | new GrammarLine("List remotes", new[] { "remote", "-v"}, (git, args) => { git.Remotes.List(); }),
46 | new GrammarLine("Add remote", new[] { "remote", "add", "", ""}, (git, args) => { git.Remotes.Remotes.Add(new Remote(){Name = args[2], Url = new Uri(args[3])}); }),
47 | new GrammarLine("Remove remote", new[] { "remote", "rm", ""}, (git, args) => { git.Remotes.Remove(args[2]); }),
48 | };
49 |
50 | public string Handle(KBGit git, GrammarLine[] config, string[] cmdParams)
51 | {
52 | var match = config.SingleOrDefault(x => x.Grammar.Length == cmdParams.Length
53 | && x.Grammar.Zip(cmdParams, (gramar, arg) => gramar.StartsWith("<") || gramar == arg).All(m => m));
54 |
55 | if (match == null)
56 | return $"KBGit Help\r\n----------\r\ngit {string.Join("\r\ngit ", config.Select(x => $"{string.Join(" ", x.Grammar),-34} - {x.Explanation}."))}";
57 |
58 | git.LoadState();
59 | var result = match.ActionOnMatch(git, cmdParams);
60 | git.StoreState();
61 |
62 | return result;
63 | }
64 | }
65 |
66 | ///
67 | /// Mini clone of git
68 | /// Supporting
69 | /// * commits
70 | /// * branches
71 | /// * detached heads
72 | /// * checkout old commits
73 | /// * logging
74 | ///
75 | public class KBGit
76 | {
77 | public const string KbGitDataFile = ".git";
78 | public string CodeFolder { get; }
79 | public Storage Hd;
80 | public RemotesHandling Remotes;
81 | public BranchHandling Branches;
82 |
83 | public KBGit(string startpath)
84 | {
85 | CodeFolder = startpath;
86 | }
87 |
88 | public string GitStateFile => Path.Combine(CodeFolder, ".git");
89 |
90 | public void LoadState()
91 | {
92 | if (File.Exists(GitStateFile))
93 | {
94 | Hd = ByteHelper.Deserialize(File.ReadAllBytes(GitStateFile));
95 | Remotes = new RemotesHandling(Hd.Remotes);
96 | Branches = new BranchHandling(Hd, CodeFolder);
97 | }
98 | }
99 |
100 | public void StoreState()
101 | {
102 | if (Hd != null)
103 | File.WriteAllBytes(GitStateFile, ByteHelper.Serialize(Hd));
104 | }
105 |
106 | ///
107 | /// Initialize a repo. eg. "git init"
108 | ///
109 | public string InitializeRepository()
110 | {
111 | Hd = new Storage();
112 | Remotes = new RemotesHandling(Hd.Remotes);
113 | Branches = new BranchHandling(Hd, CodeFolder);
114 | Branches.CreateBranch("master", null);
115 | return "Initialized empty Git repository";
116 | }
117 |
118 | ///
119 | /// Simulate syntax: e.g. "HEAD~2"
120 | ///
121 | public Id HeadRef(int numberOfPredecessors)
122 | {
123 | var result = Hd.Head.GetId(Hd);
124 | for (int i = 0; i < numberOfPredecessors; i++)
125 | {
126 | result = Hd.Commits[result].Parents.First();
127 | }
128 |
129 | return result;
130 | }
131 |
132 | ///
133 | /// Equivalent to "git hash-object -w "
134 | ///
135 | public Id HashObject(string content) => Id.HashObject(content);
136 |
137 | public Id Commit(string message, string author, DateTime now)
138 | {
139 | var composite = FileSystemScanFolder(CodeFolder);
140 | composite.Visit(x =>
141 | {
142 | if (x is TreeTreeLine t)
143 | Hd.Trees.TryAdd(t.Id, t.Tree);
144 | if (x is BlobTreeLine b)
145 | Hd.Blobs.TryAdd(b.Id, b.Blob);
146 | });
147 |
148 | var parentCommitId = Hd.Head.GetId(Hd);
149 | var isFirstCommit = parentCommitId == null;
150 | var commit = new CommitNode
151 | {
152 | Time = now,
153 | Tree = composite.Tree,
154 | TreeId = composite.Id,
155 | Author = author,
156 | Message = message,
157 | Parents = isFirstCommit ? new Id[0] : new[] { parentCommitId },
158 | };
159 |
160 | var commitId = Id.HashObject(commit);
161 | Hd.Commits.Add(commitId, commit);
162 |
163 | if (Hd.Head.IsDetachedHead())
164 | Hd.Head.Update(commitId, Hd);
165 | else
166 | Hd.Branches[Hd.Head.Branch].Tip = commitId;
167 |
168 | return commitId;
169 | }
170 |
171 | internal void AddOrSetBranch(string branch, Branch branchInfo)
172 | {
173 | if (Hd.Branches.ContainsKey(branch))
174 | Hd.Branches[branch].Tip = branchInfo.Tip;
175 | else
176 | Hd.Branches.Add(branch, branchInfo);
177 | }
178 |
179 | /// eg. "git log"
180 | public string Log()
181 | {
182 | var sb = new StringBuilder();
183 | foreach (var branch in Hd.Branches)
184 | {
185 | sb.AppendLine($"\r\nLog for {branch.Key}");
186 |
187 | if (branch.Value.Tip == null) // empty repo
188 | continue;
189 |
190 | var nodes = GetReachableNodes(branch.Value.Tip, branch.Value.Created);
191 | foreach (var comit in nodes.OrderByDescending(x => x.Value.Time))
192 | {
193 | var commitnode = comit.Value;
194 | var key = comit.Key.ToString();
195 | var msg = commitnode.Message.Substring(0, Math.Min(40, commitnode.Message.Length));
196 | var author = $"{commitnode.Author}";
197 |
198 | sb.AppendLine($"* {key} - {msg} ({commitnode.Time:yyyy\\/MM\\/dd hh\\:mm\\:ss}) <{author}> ");
199 | }
200 | }
201 |
202 | return sb.ToString();
203 | }
204 |
205 | /// Clean out unreferences nodes. Equivalent to "git gc"
206 | public void Gc()
207 | {
208 | var reachables = Hd.Branches.Select(x => x.Value.Tip)
209 | .Union(new[] { Hd.Head.GetId(Hd) })
210 | .SelectMany(x => GetReachableNodes(x))
211 | .Select(x => x.Key);
212 |
213 | var deletes = Hd.Commits.Select(x => x.Key)
214 | .Except(reachables).ToList();
215 |
216 | deletes.ForEach(x => Hd.Commits.Remove(x));
217 | }
218 |
219 | internal void RawImportCommits(KeyValuePair[] commits, string branch, Branch branchInfo)
220 | {
221 | //Console.WriteLine("RawImportCommits");
222 |
223 | foreach (var commit in commits)
224 | {
225 | //Console.WriteLine("import c" + commit.Key);
226 | Hd.Commits.TryAdd(commit.Key, commit.Value);
227 | Hd.Trees.TryAdd(commit.Value.TreeId, commit.Value.Tree);
228 |
229 | foreach (var treeLine in commit.Value.Tree.Lines)
230 | {
231 | if (treeLine is BlobTreeLine b)
232 | {
233 | //Console.WriteLine("import b " + b.Id);
234 | Hd.Blobs.TryAdd(b.Id, b.Blob);
235 | }
236 |
237 | if (treeLine is TreeTreeLine t)
238 | {
239 | //Console.WriteLine("import t " + t.Id);
240 | Hd.Trees.TryAdd(t.Id, t.Tree);
241 | }
242 | }
243 | }
244 |
245 | AddOrSetBranch(branch, branchInfo);
246 | }
247 |
248 | public List> GetReachableNodes(Id from, Id downTo = null)
249 | {
250 | var result = new List>();
251 | GetReachableNodes(from);
252 |
253 | void GetReachableNodes(Id currentId)
254 | {
255 | var commit = Hd.Commits[currentId];
256 | result.Add(new KeyValuePair(currentId, commit));
257 |
258 | foreach (var parent in commit.Parents.Where(x => !x.Equals(downTo)))
259 | {
260 | GetReachableNodes(parent);
261 | }
262 | }
263 |
264 | return result;
265 | }
266 |
267 | public TreeTreeLine FileSystemScanFolder(string path) => MakeTreeTreeLine(path);
268 |
269 | public ITreeLine[] FileSystemScanSubFolder(string path)
270 | {
271 | var entries = new DirectoryInfo(path).EnumerateFileSystemInfos("*", SearchOption.TopDirectoryOnly).ToArray();
272 |
273 | var lines = new List(entries.OfType()
274 | .Where(x => x.FullName != Path.Combine(CodeFolder, ".git"))
275 | .Select(x => new { Content = File.ReadAllText(x.FullName), x.FullName })
276 | .Select(x => new BlobTreeLine(new Id(ByteHelper.ComputeSha(x.Content)), new BlobNode(x.Content), x.FullName.Substring(CodeFolder.Length+1))));
277 |
278 | lines.AddRange(entries.OfType()
279 | .Select(x => MakeTreeTreeLine(EnsurePathEndsInSlash(x.FullName))));
280 |
281 | return lines.ToArray();
282 | }
283 |
284 | private TreeTreeLine MakeTreeTreeLine(string path)
285 | {
286 | var folderentries = FileSystemScanSubFolder(path);
287 | var treenode = new TreeNode(folderentries);
288 | var id = Id.HashObject(folderentries);
289 |
290 | return new TreeTreeLine(id, treenode, EnsurePathEndsInSlash(path).Substring(CodeFolder.Length+1));
291 | }
292 |
293 | public string EnsurePathEndsInSlash(string folderPath) => folderPath.EndsWith(Path.DirectorySeparatorChar) ? folderPath : folderPath + Path.DirectorySeparatorChar;
294 | }
295 |
296 | public class BranchHandling
297 | {
298 | private readonly Storage Hd;
299 | private readonly string codeFolder;
300 |
301 | public BranchHandling(Storage hd, string codeFolder)
302 | {
303 | Hd = hd;
304 | this.codeFolder = codeFolder;
305 | }
306 |
307 | /// Create a branch: e.g "git checkout -b foo"
308 | public string CreateBranch(string name) => CreateBranch(name, Hd.Head.GetId(Hd));
309 |
310 | /// Create a branch: e.g "git checkout -b foo fb1234.."
311 | public string CreateBranch(string name, Id position)
312 | {
313 | Hd.Branches.Add(name, new Branch(position, position));
314 | Hd.ResetCodeFolder(codeFolder, position);
315 | Hd.Head.Update(name, Hd);
316 | return $"Switched to a new branch '{name}'";
317 | }
318 |
319 | ///
320 | /// return all branches and highlight current branch: "git branch"
321 | ///
322 | public string ListBranches()
323 | {
324 | var branched = Hd.Branches
325 | .OrderBy(x => x.Key)
326 | .Select(x => $"{(Hd.Head.Branch == x.Key ? "*" : " ")} {x.Key}");
327 |
328 | var detached = Hd.Head.IsDetachedHead() ? $"* (HEAD detached at {Hd.Head.Id.ToString().Substring(0, 7)})\r\n" : "";
329 |
330 | return detached + string.Join("\r\n", branched);
331 | }
332 |
333 | ///
334 | /// Delete a branch. eg. "git branch -D name"
335 | ///
336 | public string DeleteBranch(string branch)
337 | {
338 | if(Hd.Head.Branch == branch)
339 | throw new Exception($"error: Cannot delete branch '{branch}' checked out");
340 |
341 | var id = Hd.Head.GetId(Hd);
342 | Hd.Branches.Remove(branch);
343 | return $"Deleted branch {branch} (was {id.ShaId}).";
344 | }
345 |
346 | ///
347 | /// Change HEAD to branch,e.g. "git checkout featurebranch"
348 | ///
349 | public string Checkout(string branch)
350 | {
351 | Checkout(Hd.Branches[branch].Tip);
352 | return $"Switched to a new branch {branch}";
353 | }
354 |
355 | ///
356 | /// Change folder content to commit position and move HEAD
357 | ///
358 | public string Checkout(Id id)
359 | {
360 | Hd.ResetCodeFolder(codeFolder, id);
361 | return Hd.Head.Update(id, Hd);
362 | }
363 |
364 | public void ResetBranchPointer(string branch, Id newTip) => Hd.Branches[branch].Tip = newTip;
365 | }
366 |
367 | public static class ByteHelper
368 | {
369 | static readonly SHA256 Sha = SHA256.Create();
370 |
371 | public static string ComputeSha(object o) => string.Join("", Sha.ComputeHash(Serialize(o)).Select(x => String.Format("{0:x2}", x)));
372 |
373 | public static byte[] Serialize(object o)
374 | {
375 | using (var stream = new MemoryStream())
376 | {
377 | new BinaryFormatter().Serialize(stream, o);
378 | stream.Seek(0, SeekOrigin.Begin);
379 | return stream.GetBuffer();
380 | }
381 | }
382 |
383 | public static T Deserialize(Stream s) where T : class => (T)new BinaryFormatter().Deserialize(s);
384 |
385 | public static T Deserialize(byte[] o) where T : class
386 | {
387 | using (var ms = new MemoryStream(o))
388 | {
389 | return (T) new BinaryFormatter().Deserialize(ms);
390 | }
391 | }
392 | }
393 |
394 | public class Fileinfo
395 | {
396 | public readonly string Path;
397 | public readonly string Content;
398 |
399 | public Fileinfo(string path, string content)
400 | {
401 | Path = path;
402 | Content = content;
403 | }
404 | }
405 |
406 | [Serializable]
407 | public class Id
408 | {
409 | public string ShaId { get; private set; }
410 |
411 | public Id(string sha)
412 | {
413 | if(sha == null || sha.Length != 64)
414 | throw new ArgumentException("Not a valid SHA");
415 | ShaId = sha;
416 | }
417 |
418 | ///
419 | /// Equivalent to "git hash-object -w "
420 | ///
421 | public static Id HashObject(object o) => new Id(ByteHelper.ComputeSha(o));
422 |
423 | public override string ToString() => ShaId;
424 | public override bool Equals(object obj) => ShaId.Equals((obj as Id)?.ShaId);
425 | public override int GetHashCode() => ShaId.GetHashCode();
426 | }
427 |
428 | [Serializable]
429 | public class Storage
430 | {
431 | public Dictionary Blobs = new Dictionary();
432 | public Dictionary Trees = new Dictionary();
433 | public Dictionary Commits = new Dictionary();
434 |
435 | public Dictionary Branches = new Dictionary();
436 | public Head Head = new Head();
437 | public List Remotes = new List();
438 |
439 | internal void ResetCodeFolder(string codeFolder, Id position)
440 | {
441 | Directory.EnumerateDirectories(codeFolder).Where(x=>{ Console.WriteLine($"delete '{x}'"); return true;}).ToList().ForEach(x=>Directory.Delete(x, true));
442 | Directory.EnumerateFiles(codeFolder).Where(x => x != Path.Combine(codeFolder, ".git")).Where(x => { Console.WriteLine($"delete '{x}'"); return true; }).ToList().ForEach(x => File.Delete(x));
443 |
444 | if (position != null)
445 | {
446 | var commit = Commits[position];
447 | foreach (BlobTreeLine line in commit.Tree.Lines)
448 | {
449 | //var path = Path.Combine(codeFolder, line.Path);
450 | //Console.WriteLine($"Restoring \'{path}\' <- '{codeFolder}' + '{line.Path}'");
451 | File.WriteAllText(Path.Combine(codeFolder, line.Path), line.Blob.Content);
452 | }
453 | }
454 | }
455 | }
456 |
457 | [Serializable]
458 | public class Branch
459 | {
460 | public Id Created { get; }
461 | public Id Tip { get; set; }
462 |
463 | public Branch(Id created, Id tip)
464 | {
465 | Created = created;
466 | Tip = tip;
467 | }
468 | }
469 |
470 | [Serializable]
471 | public class Remote
472 | {
473 | public string Name;
474 | public Uri Url;
475 | }
476 |
477 | public class RemotesHandling
478 | {
479 | public readonly List Remotes;
480 |
481 | public RemotesHandling(List remotes)
482 | {
483 | Remotes = remotes;
484 | }
485 |
486 | ///
487 | /// List remotes Git remote -v
488 | ///
489 | public string List() => string.Join("\r\n", Remotes.Select(x => $"{x.Name,-12} {x.Url}"));
490 |
491 | ///
492 | /// Remove a remote
493 | ///
494 | public void Remove(string name) => Remotes.RemoveAll(x => x.Name == name);
495 | }
496 |
497 | ///
498 | /// In git the file content of the file "HEAD" is either an ID or a reference to a branch.eg.
499 | /// "ref: refs/heads/master"
500 | ///
501 | [Serializable]
502 | public class Head
503 | {
504 | public Id Id { get; private set; }
505 | public string Branch { get; private set; }
506 |
507 | public void Update(string branch, Storage s)
508 | {
509 | if (!s.Branches.ContainsKey(branch))
510 | throw new ArgumentOutOfRangeException($"No branch named \'{branch}\'");
511 |
512 | Branch = branch;
513 | Id = null;
514 | }
515 |
516 | public string Update(Id position, Storage s)
517 | {
518 | var b = s.Branches.FirstOrDefault(x => x.Value.Tip.Equals(position));
519 | if(b.Key == null)
520 | {
521 | if (!s.Commits.ContainsKey(position))
522 | throw new ArgumentOutOfRangeException($"No commit with id '{position}'");
523 |
524 | Branch = null;
525 | Id = position;
526 | return "You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout.";
527 | }
528 |
529 | Update(b.Key, s);
530 | return null;
531 | }
532 |
533 | public bool IsDetachedHead() => Id != null;
534 |
535 | public Id GetId(Storage s) => Id ?? s.Branches[Branch].Tip;
536 | }
537 |
538 | [Serializable]
539 | public class TreeNode
540 | {
541 | public ITreeLine[] Lines;
542 | public TreeNode(ITreeLine[] lines)
543 | {
544 | Lines = lines;
545 | }
546 |
547 | public override string ToString() => string.Join("\n", Lines.Select(x => x.ToString()));
548 | }
549 |
550 | public interface ITreeLine
551 | {
552 | void Visit(Action code);
553 | }
554 |
555 | [Serializable]
556 | public class BlobTreeLine : ITreeLine
557 | {
558 | public Id Id { get; private set; }
559 | public BlobNode Blob { get; private set; }
560 | public string Path { get; private set; }
561 |
562 | public BlobTreeLine(Id id, BlobNode blob, string path)
563 | {
564 | Id = id;
565 | Blob = blob;
566 | Path = path;
567 | }
568 |
569 | public override string ToString() => $"blob {Path}";
570 |
571 | public void Visit(Action code) => code(this);
572 | }
573 |
574 | [Serializable]
575 | public class TreeTreeLine : ITreeLine
576 | {
577 | public Id Id { get; private set; }
578 | public TreeNode Tree { get; private set; }
579 | public string Path { get; private set; }
580 |
581 | public TreeTreeLine(Id id, TreeNode tree, string path)
582 | {
583 | Id = id;
584 | Tree = tree;
585 | Path = path;
586 | }
587 |
588 | public override string ToString() => $"tree {Tree.Lines.Length} {Path}\r\n{string.Join("\r\n", Tree.Lines.Select(x => x.ToString()))}";
589 |
590 | public void Visit(Action code)
591 | {
592 | code(this);
593 |
594 | foreach (var line in Tree.Lines)
595 | line.Visit(code);
596 | }
597 | }
598 |
599 | [Serializable]
600 | public class CommitNode
601 | {
602 | public DateTime Time;
603 | public TreeNode Tree;
604 | public Id TreeId;
605 | public string Author;
606 | public string Message;
607 | public Id[] Parents = new Id[0];
608 | }
609 |
610 | [Serializable]
611 | public class BlobNode
612 | {
613 | public string Content { get; }
614 |
615 | public BlobNode(string content) => Content = content;
616 | }
617 |
618 | public class Differ
619 | {
620 | public enum Kind { Deleted, Added }
621 |
622 | public class Result
623 | {
624 | public Tuple[] Lines;
625 | public Kind Kind;
626 | }
627 |
628 | public List Diff(string[] a, string[] b)
629 | {
630 | return Diff(a.Select((x, i) => Tuple.Create(string.Intern(x), i + 1)).ToArray(),
631 | b.Select((x, i) => Tuple.Create(string.Intern(x), i + 1)).ToArray())
632 | .Where(x => x.Lines.Any()).ToList();
633 | }
634 |
635 | IEnumerable Diff(Tuple[] a, Tuple[] b)
636 | {
637 | int longestOverlap = 0, offsetA = -1, offsetB = -1, overlap;
638 | for (int ia = 0; ia < a.Length; ia++)
639 | {
640 | for (int ib = 0; ib < b.Length; ib++)
641 | {
642 | for (overlap = 0; ia + overlap < a.Length && ib + overlap < b.Length && a[ia + overlap].Item1 == b[ib + overlap].Item1; overlap++)
643 | ;
644 |
645 | if (overlap > longestOverlap)
646 | {
647 | longestOverlap = overlap; offsetA = ia; offsetB = ib;
648 | }
649 | }
650 | }
651 |
652 | if (longestOverlap == 0)
653 | return new[] {new Result {Kind = Kind.Deleted, Lines = a}, new Result {Kind = Kind.Added, Lines = b}};
654 |
655 | return Diff(a.Take(offsetA).ToArray(), b.Take(offsetB).ToArray())
656 | .Union(Diff(a.Skip(offsetA + longestOverlap).ToArray(), b.Skip(offsetB + longestOverlap).ToArray()));
657 | }
658 | }
659 |
660 | [Serializable]
661 | public class GitPushBranchRequest
662 | {
663 | public KeyValuePair[] Commits { get; set; }
664 | public string Branch { get; set; }
665 | public Branch BranchInfo { get; set; }
666 | public Id LatestRemoteBranchPosition { get; set; }
667 | }
668 |
669 | [Serializable]
670 | public class GitPullResponse
671 | {
672 | public KeyValuePair[] Commits { get; set; }
673 | public Branch BranchInfo { get; set; }
674 | }
675 |
676 | ///
677 | /// Used for communicating with a git server
678 | ///
679 | public class GitNetworkClient
680 | {
681 | public void PushBranch(Remote remote, string branch, Branch branchInfo, Id fromPosition, KeyValuePair[] nodes)
682 | {
683 | var request = new GitPushBranchRequest() {Branch = branch, BranchInfo = branchInfo, LatestRemoteBranchPosition = fromPosition, Commits = nodes};
684 | var result = new HttpClient().PostAsync(remote.Url, new ByteArrayContent(ByteHelper.Serialize(request))).GetAwaiter().GetResult();
685 | Console.WriteLine($"Push status: {result.StatusCode}");
686 | }
687 |
688 | public Id PullBranch(Remote remote, string branch, KBGit git)
689 | {
690 | var bytes = new HttpClient().GetByteArrayAsync(remote.Url + "?branch=" + branch).GetAwaiter().GetResult();
691 | var commits = ByteHelper.Deserialize(bytes);
692 | git.RawImportCommits(commits.Commits, $"{remote.Name}/{branch}", commits.BranchInfo);
693 | return commits.BranchInfo.Tip;
694 | }
695 |
696 | public void CloneBranch(KBGit git, string remotename, string url, string branch)
697 | {
698 | git.InitializeRepository();
699 | git.Remotes.Remotes.Add(new Remote { Name = remotename, Url = new Uri(url)});
700 | var tip = PullBranch(git.Remotes.Remotes.Single(), branch, git);
701 | git.Branches.ResetBranchPointer("master", tip);
702 | git.Branches.Checkout("master");
703 | }
704 | }
705 |
706 | public class GitServer
707 | {
708 | private readonly KBGit git;
709 | private HttpListener listener;
710 | public bool? Running { get; private set; }
711 |
712 | public GitServer(KBGit git)
713 | {
714 | this.git = git;
715 | }
716 |
717 | public void Abort()
718 | {
719 | Running = false;
720 | listener?.Abort();
721 | }
722 |
723 | public void StartDaemon(int port)
724 | {
725 | Console.WriteLine($"Serving on http://localhost:{port}/");
726 |
727 | listener = new HttpListener();
728 | listener.Prefixes.Add($"http://localhost:{port}/");
729 | listener.Start();
730 |
731 | Running = true;
732 | while (Running.Value)
733 | {
734 | var context = listener.GetContext();
735 | try
736 | {
737 | if (context.Request.HttpMethod == "GET")
738 | {
739 | var branch = context.Request.QueryString.Get("branch");
740 |
741 | if (!git.Hd.Branches.ContainsKey(branch))
742 | {
743 | context.Response.StatusCode = 404;
744 | context.Response.Close();
745 | continue;
746 | }
747 |
748 | context.Response.Close(ByteHelper.Serialize(new GitPullResponse()
749 | {
750 | BranchInfo = git.Hd.Branches[branch],
751 | Commits = git.GetReachableNodes(git.Hd.Branches[branch].Tip).ToArray()
752 | }), true);
753 | }
754 |
755 | if(context.Request.HttpMethod == "POST")
756 | {
757 | var req = ByteHelper.Deserialize(context.Request.InputStream);
758 | // todo check if we are loosing commits when updating the branch pointer..we get a fromid with the request
759 | git.RawImportCommits(req.Commits, req.Branch, req.BranchInfo);
760 | context.Response.Close();
761 | }
762 | }
763 | catch (Exception e)
764 | {
765 | Console.WriteLine($"\n\n{DateTime.Now}\n{e} - {e.Message}");
766 | context.Response.StatusCode = 500;
767 | context.Response.Close();
768 | }
769 | }
770 | }
771 | }
772 | }
--------------------------------------------------------------------------------