├── .gitignore ├── CONTRIBUTING.md ├── Git.cs ├── ISSUE_TEMPLATE.md ├── KBGit.csproj ├── KBGit.sln ├── LICENSE ├── Program.cs ├── README.md ├── kbgit.tests ├── KBGitTests.cs ├── ReadmeHelper.cs ├── RepoBuilder.cs ├── XunitConfiguration.cs └── kbgit.tests.csproj ├── kbgithelper.ps1 └── makerelease.ps1 /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KBGit - Git implemented from scratch in 500 lines of code (or less ...) 2 | 3 | Project statistics: 4 | [![Stats](https://img.shields.io/badge/Code_lines-382-ff69b4.svg)]() 5 | [![Stats](https://img.shields.io/badge/Doc_lines-25-ff69b4.svg)]() 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.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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /kbgit.tests/XunitConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | [assembly: CollectionBehavior(DisableTestParallelization = true, MaxParallelThreads = 1)] 4 | 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /makerelease.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kbilsted/KBGit/2a7e0168cf37caed6fc9063e430ca6b6ce226cc4/makerelease.ps1 --------------------------------------------------------------------------------