├── GitCandy.Web ├── App_GlobalResources │ └── SR.zh-Hans.Designer.cs ├── wwwroot │ ├── Scripts │ │ └── _references.js │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── Content │ │ ├── highlight.css │ │ ├── Site.css │ │ └── Cube.css ├── Views │ ├── _ViewStart.cshtml │ ├── Shared │ │ ├── DisplayTemplates │ │ │ ├── YesNo.cshtml │ │ │ ├── Members.cshtml │ │ │ └── Maps.cshtml │ │ ├── Error.cshtml │ │ ├── _RepositoryLink.cshtml │ │ ├── _Pager.cshtml │ │ └── _FrontLayout.cshtml │ ├── Account │ │ ├── Forgot.cshtml │ │ ├── Delete.cshtml │ │ ├── Login.cshtml │ │ ├── Change.cshtml │ │ ├── Detail.cshtml │ │ ├── Index.cshtml │ │ ├── Create.cshtml │ │ └── Edit.cshtml │ ├── Repository │ │ ├── _ZipButton.cshtml │ │ ├── _BlobPreview.cshtml │ │ ├── Delete.cshtml │ │ ├── _GitUrlButton.cshtml │ │ ├── _PathBar.cshtml │ │ ├── Tags.cshtml │ │ ├── Compare.cshtml │ │ ├── Commit.cshtml │ │ ├── Commits.cshtml │ │ ├── _Diff.cshtml │ │ ├── Blame.cshtml │ │ ├── Blob.cshtml │ │ ├── Branches.cshtml │ │ ├── Contributors.cshtml │ │ ├── Detail.cshtml │ │ ├── Edit.cshtml │ │ ├── Index.cshtml │ │ ├── _BranchSelector.cshtml │ │ └── Create.cshtml │ ├── _ViewImports.cshtml │ ├── Team │ │ ├── Delete.cshtml │ │ ├── Detail.cshtml │ │ ├── Index.cshtml │ │ ├── Create.cshtml │ │ ├── Edit.cshtml │ │ └── Users.cshtml │ └── Home │ │ └── About.cshtml ├── favicon.ico ├── Models │ ├── BlobType.cs │ ├── ContributorsModel.cs │ ├── GitUrl.cs │ ├── AheadBehindModel.cs │ ├── TeamListModel.cs │ ├── UserListModel.cs │ ├── BranchesModel.cs │ ├── TagsModel.cs │ ├── TagModel.cs │ ├── RepositoryScope.cs │ ├── SshModel.cs │ ├── RepositoryModelBase.cs │ ├── RepositoryListModel.cs │ ├── PathBarModel.cs │ ├── BranchSelectorModel.cs │ ├── BlameHunkModel.cs │ ├── CommitChangeModel.cs │ ├── CompareModel.cs │ ├── BlameModel.cs │ ├── LoginModel.cs │ ├── CommitsModel.cs │ ├── CommitModel.cs │ ├── TreeModel.cs │ ├── CollaborationModel.cs │ ├── TreeEntryModel.cs │ ├── RepositoryStatisticsModel.cs │ ├── ChangePasswordModel.cs │ ├── TeamModel.cs │ ├── SettingModel.cs │ ├── RepositoryModel.cs │ └── UserModel.cs ├── .config │ └── dotnet-tools.json ├── Git │ ├── Cache │ │ └── GitCacheReturn.cs │ ├── RevisionSummaryCacheItem.cs │ ├── RepositorySizeAccessor.cs │ ├── CommitsAccessor.cs │ ├── ScopeAccessor.cs │ ├── LastCommitAccessor.cs │ ├── HistoryDivergenceAccessor.cs │ ├── BlameAccessor.cs │ ├── ArchiverAccessor.cs │ ├── ContributorsAccessor.cs │ └── SummaryAccessor.cs ├── appsettings.Development.json ├── Areas │ └── GitCandy │ │ ├── Controllers │ │ ├── GitController.cs │ │ ├── UserTeamController.cs │ │ ├── UserRepositoryController.cs │ │ ├── AuthorizationLogController.cs │ │ ├── RepositoryController.cs │ │ ├── GitHistoryController.cs │ │ └── UserController.cs │ │ ├── GitCandyArea.cs │ │ └── Views │ │ ├── User │ │ └── _List_Search.cshtml │ │ └── Repository │ │ └── _List_Search.cshtml ├── Properties │ ├── launchSettings.json │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── Extensions │ ├── MemberHelper.cs │ ├── CommitLogExtension.cs │ └── HtmlHelperExtension.cs ├── Base │ ├── GitUrlConstraint.cs │ ├── UserUrlConstraint.cs │ └── RawResult.cs ├── Controllers │ ├── HomeController.cs │ ├── AccountController.cs │ └── CandyControllerBase.cs ├── appsettings.json ├── GitCandy.Web.csproj ├── Services │ └── AccountService.cs └── GitSetting.cs ├── .editorconfig ├── Doc ├── clover.exe └── pack.bat ├── GitCandy ├── Entity │ ├── xcodetool.exe │ └── Entity │ │ ├── SSH密钥.Biz.cs │ │ ├── 认证日志.Biz.cs │ │ └── 用户仓库.Biz.cs ├── Base │ ├── RegularExpression.cs │ ├── Profiler.cs │ ├── Pager.cs │ └── StringLogicalComparer.cs ├── GitCandy.csproj └── Security │ └── Token.cs ├── Test ├── Test.csproj └── Program.cs ├── LICENSE.md ├── GitCandy.sln ├── README.md ├── .gitattributes └── .gitignore /GitCandy.Web/App_GlobalResources/SR.zh-Hans.Designer.cs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/.editorconfig -------------------------------------------------------------------------------- /Doc/clover.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/Doc/clover.exe -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/Scripts/_references.js: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Views/Shared/_FrontLayout.cshtml"; 3 | } -------------------------------------------------------------------------------- /GitCandy.Web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/GitCandy.Web/favicon.ico -------------------------------------------------------------------------------- /GitCandy/Entity/xcodetool.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/GitCandy/Entity/xcodetool.exe -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/DisplayTemplates/YesNo.cshtml: -------------------------------------------------------------------------------- 1 | @model bool 2 | 3 | @(Model ? SR.Shared_Yes : SR.Shared_No) -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NewLifeX/GitCandy/HEAD/GitCandy.Web/wwwroot/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /GitCandy.Web/Models/BlobType.cs: -------------------------------------------------------------------------------- 1 | namespace GitCandy.Models 2 | { 3 | public enum BlobType 4 | { 5 | Binary, 6 | Text, 7 | MarkDown, 8 | Image, 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/ContributorsModel.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCandy.Models 3 | { 4 | public class ContributorsModel : RepositoryModelBase 5 | { 6 | public RepositoryStatisticsModel Statistics { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /GitCandy.Web/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "6.0.10", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /GitCandy.Web/Git/Cache/GitCacheReturn.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCandy.Git.Cache 3 | { 4 | public struct GitCacheReturn 5 | { 6 | public T Value { get; set; } 7 | public Boolean Done { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/GitUrl.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class GitUrl 7 | { 8 | public String Type { get; set; } 9 | public String Url { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /GitCandy.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Forgot.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Account_ForgotTitle); 3 | } 4 | 5 |

@SR.Account_ForgotTitle

6 | 7 |

Please contact with administrator!

8 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/AheadBehindModel.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCandy.Models 3 | { 4 | public class AheadBehindModel 5 | { 6 | public Int32 Ahead { get; set; } 7 | public Int32 Behind { get; set; } 8 | public CommitModel Commit { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/_ZipButton.cshtml: -------------------------------------------------------------------------------- 1 | @model String 2 | 3 | 6 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/DisplayTemplates/Members.cshtml: -------------------------------------------------------------------------------- 1 | @model String[] 2 | 3 | @{ 4 | } 5 | 6 | @for (var i = 0; i < Model.Length; i++) 7 | { 8 | var ss = Model[i].Split("@"); 9 | if (i != 0) 10 | { , } 11 | @ss[1] 12 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/TeamListModel.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCandy.Models 3 | { 4 | public class TeamListModel 5 | { 6 | public TeamModel[] Teams { get; set; } 7 | public Int32 CurrentPage { get; set; } 8 | public Int32 ItemCount { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/UserListModel.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCandy.Models 3 | { 4 | public class UserListModel 5 | { 6 | public UserModel[] Users { get; set; } 7 | public Int32 CurrentPage { get; set; } 8 | public Int32 ItemCount { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/GitController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using NewLife.Cube; 3 | 4 | namespace GitCandy.Web.Areas.GitCandy.Controllers 5 | { 6 | [GitCandyArea] 7 | [DisplayName("糖果配置")] 8 | public class GitController : ConfigController { } 9 | } -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/UserTeamController.cs: -------------------------------------------------------------------------------- 1 | using NewLife.Cube; 2 | using NewLife.GitCandy.Entity; 3 | 4 | namespace GitCandy.Web.Areas.GitCandy.Controllers 5 | { 6 | [GitCandyArea] 7 | public class UserTeamController : EntityController 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/DisplayTemplates/Maps.cshtml: -------------------------------------------------------------------------------- 1 | @model IDictionary 2 | 3 | @{ 4 | var i = 0; 5 | } 6 | 7 | @foreach (var item in Model) 8 | { 9 | if (i++ != 0) 10 | { 11 | , 12 | } 13 | @(item.Value ?? item.Key) 14 | } -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/UserRepositoryController.cs: -------------------------------------------------------------------------------- 1 | using NewLife.Cube; 2 | using NewLife.GitCandy.Entity; 3 | 4 | namespace GitCandy.Web.Areas.GitCandy.Controllers 5 | { 6 | [GitCandyArea] 7 | public class UserRepositoryController : EntityController 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/AuthorizationLogController.cs: -------------------------------------------------------------------------------- 1 | using NewLife.Cube; 2 | using NewLife.GitCandy.Entity; 3 | 4 | namespace GitCandy.Web.Areas.GitCandy.Controllers 5 | { 6 | [GitCandyArea] 7 | public class AuthorizationLogController : EntityController 8 | { 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/BranchesModel.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace GitCandy.Models 3 | { 4 | public class BranchesModel : RepositoryModelBase 5 | { 6 | public CommitModel Commit { get; set; } 7 | public AheadBehindModel[] AheadBehinds { get; set; } 8 | public Boolean CanDelete { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/TagsModel.cs: -------------------------------------------------------------------------------- 1 | namespace GitCandy.Models 2 | { 3 | public class TagsModel : RepositoryModelBase 4 | { 5 | public TagModel[] Tags { get; set; } 6 | public Boolean HasTags => Tags != null && Tags.Length != 0; 7 | 8 | public Boolean CanDelete { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using NewLife 2 | @using NewLife.Cube 3 | @using NewLife.Reflection 4 | @using NewLife.Web 5 | @using XCode.Membership 6 | @using NewLife.Cube.ViewModels 7 | @using GitCandy.Web.Extensions 8 | @using GitCandy.Web.App_GlobalResources 9 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 10 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/TagModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Models 4 | { 5 | public class TagModel 6 | { 7 | public String ReferenceName { get; set; } 8 | public String Sha { get; set; } 9 | public DateTimeOffset When { get; set; } 10 | public String MessageShort { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /GitCandy.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "GitCandy.Web": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:7001;http://localhost:7000" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/GitCandyArea.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using NewLife; 3 | using NewLife.Cube; 4 | 5 | namespace GitCandy.Web.Areas.GitCandy; 6 | 7 | [DisplayName("糖果仓库")] 8 | [Menu(-1, true, Icon = "fa-desktop")] 9 | public class GitCandyArea : AreaBase 10 | { 11 | public GitCandyArea() : base(nameof(GitCandyArea).TrimEnd("Area")) { } 12 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/RepositoryScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Models 4 | { 5 | [Serializable] 6 | public class RepositoryScope 7 | { 8 | public Int32 Commits { get; set; } 9 | public Int32 Branches { get; set; } 10 | public Int32 Tags { get; set; } 11 | public Int32 Contributors { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/SshModel.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class SshModel 7 | { 8 | public String Username { get; set; } 9 | public SshKey[] SshKeys { get; set; } 10 | 11 | public class SshKey 12 | { 13 | public String Name { get; set; } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = null; 3 | } 4 | 5 | 6 | 7 | 8 | 9 | Error 10 | 11 | 12 |
13 |

Error.

14 |

An error occurred while processing your request.

15 |
16 | 17 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/RepositoryModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Models 4 | { 5 | public abstract class RepositoryModelBase 6 | { 7 | /// 仓库编号 8 | public Int32 Id { get; set; } 9 | 10 | public String Owner { get; set; } 11 | public String Name { get; set; } 12 | public BranchSelectorModel BranchSelector { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/RepositoryListModel.cs: -------------------------------------------------------------------------------- 1 | namespace GitCandy.Models 2 | { 3 | public class RepositoryListModel 4 | { 5 | public RepositoryModel[] Collaborations { get; set; } 6 | public RepositoryModel[] Repositories { get; set; } 7 | public Boolean CanCreateRepository { get; set; } 8 | 9 | public Int32 CurrentPage { get; set; } 10 | 11 | public Int32 ItemCount { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/PathBarModel.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class PathBarModel 7 | { 8 | public String Name { get; set; } 9 | public String ReferenceName { get; set; } 10 | public String ReferenceSha { get; set; } 11 | public String Path { get; set; } 12 | public String Action { get; set; } 13 | public Boolean HideLastSlash { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/BranchSelectorModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class BranchSelectorModel 7 | { 8 | public IEnumerable Branches { get; set; } 9 | public IEnumerable Tags { get; set; } 10 | public String Current { get; set; } 11 | public Boolean CurrentIsBranch { get; set; } 12 | public String Path { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/BlameHunkModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Models 4 | { 5 | [Serializable] 6 | public class BlameHunkModel 7 | { 8 | public String Code { get; set; } 9 | public String MessageShort { get; set; } 10 | public String Sha { get; set; } 11 | public String Author { get; set; } 12 | public String AuthorEmail { get; set; } 13 | public DateTimeOffset AuthorDate { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/CommitChangeModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using LibGit2Sharp; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class CommitChangeModel 7 | { 8 | public String OldPath { get; set; } 9 | public String Path { get; set; } 10 | public ChangeKind ChangeKind { get; set; } 11 | public Int32 LinesAdded { get; set; } 12 | public Int32 LinesDeleted { get; set; } 13 | public String Patch { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/CompareModel.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class CompareModel : RepositoryModelBase 7 | { 8 | public String[] Branches { get; set; } 9 | public BranchSelectorModel BaseBranchSelector { get; set; } 10 | public BranchSelectorModel CompareBranchSelector { get; set; } 11 | public CommitModel CompareResult { get; set; } 12 | public CommitModel[] Walks { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/BlameModel.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class BlameModel : RepositoryModelBase 7 | { 8 | public String ReferenceName { get; set; } 9 | public String Path { get; set; } 10 | public String Sha { get; set; } 11 | public BlameHunkModel[] Hunks { get; set; } 12 | public String Brush { get; set; } 13 | public PathBarModel PathBar { get; set; } 14 | public String SizeString { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/LoginModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using GitCandy.Web.App_GlobalResources; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class LoginModel 8 | { 9 | [Display(ResourceType = typeof(SR), Name = "Account_UsernameOrEmail")] 10 | public String ID { get; set; } 11 | 12 | [Display(ResourceType = typeof(SR), Name = "Account_Password")] 13 | [DataType(DataType.Password)] 14 | public String Password { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Doc/pack.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set name=StarWeb 4 | set clover=..\..\Tools\clover.exe 5 | if not exist "%clover%" ( 6 | set clover=..\..\Doc\clover.exe 7 | ) 8 | 9 | for %%f in (*.exe) do ( 10 | rem 获取文件名(去掉扩展名) 11 | set "name=%%~nf" 12 | goto :found 13 | ) 14 | 15 | :found 16 | if defined name ( 17 | del %name%.zip /f/q 18 | %clover% zip %name%.zip *.exe *.dll *.pdb appsettings.json *.runtimeconfig.json *.deps.json -r ./runtimes/win-x64/* ./runtimes/linux-x64/ 19 | ) else ( 20 | echo No exe file found in the current directory. 21 | ) 22 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/CommitsModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class CommitsModel : RepositoryModelBase 7 | { 8 | public String ReferenceName { get; set; } 9 | public String Sha { get; set; } 10 | public String Path { get; set; } 11 | public IEnumerable Commits { get; set; } 12 | public Int32 CurrentPage { get; set; } 13 | public Int32 ItemCount { get; set; } 14 | public PathBarModel PathBar { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/_RepositoryLink.cshtml: -------------------------------------------------------------------------------- 1 | @model RepositoryModelBase 2 | @using GitCandy.Models 3 | @{ 4 | var repo = Model; 5 | var user = NewLife.GitCandy.Entity.User.FindByName(repo?.Owner); 6 | } 7 | @if (user != null) 8 | { 9 | @Html.ActionLink(repo.Owner, "Detail", user.IsTeam ? "Team" : "Account", new { name = repo.Owner }, null) 10 | / 11 | @*@Html.ActionLink(repo.Name, "Tree", new { owner = repo.Owner, name = repo.Name, path = "" })*@ 12 | @Html.RouteLink(repo.Name, "UserGitWeb", new { owner = repo.Owner, name = repo.Name }) 13 | } -------------------------------------------------------------------------------- /GitCandy.Web/Extensions/MemberHelper.cs: -------------------------------------------------------------------------------- 1 | using NewLife.Model; 2 | using XCode.Membership; 3 | using UserX = NewLife.GitCandy.Entity.User; 4 | 5 | namespace GitCandy.Web.Extensions; 6 | 7 | public static class MemberHelper 8 | { 9 | /// 是否管理员 10 | /// 11 | /// 12 | public static Boolean IsAdmin(this IManageUser user) 13 | { 14 | if (user == null) return false; 15 | 16 | if (user is User au) return au.Roles.Any(e => e.IsSystem); 17 | if (user is UserX ux) return ux.IsAdmin; 18 | 19 | return false; 20 | } 21 | } -------------------------------------------------------------------------------- /GitCandy/Base/RegularExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace GitCandy.Base 5 | { 6 | public static class RegularExpression 7 | { 8 | public const String Email = @"^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$"; 9 | public const String Username = @"(?i)^[a-z][a-z0-9\-_]+$"; 10 | public const String Teamname = @"(?i)^[a-z][a-z0-9\-_]+$"; 11 | public const String Repositoryname = @"(?i)^[a-z][a-z0-9\-\._]+(? _cache = new(StringComparer.OrdinalIgnoreCase) 8 | { 9 | "info/refs","git-upload-pack","git-receive-pack" 10 | }; 11 | 12 | public Boolean Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection) 13 | { 14 | var name = values[routeKey] + ""; 15 | if (name.IsNullOrEmpty()) return false; 16 | 17 | return _cache.Contains(name); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GitCandy.Web/Git/RevisionSummaryCacheItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Git 4 | { 5 | [Serializable] 6 | public class RevisionSummaryCacheItem 7 | { 8 | public String Name; 9 | public String Path; 10 | public String TargetSha; 11 | public String CommitSha; 12 | public String MessageShort; 13 | public String AuthorName; 14 | public String AuthorEmail; 15 | public DateTimeOffset AuthorWhen; 16 | public String CommitterName; 17 | public String CommitterEmail; 18 | public DateTimeOffset CommitterWhen; 19 | public Int32 Ahead; 20 | public Int32 Behind; 21 | } 22 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/CommitModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using LibGit2Sharp; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class CommitModel : RepositoryModelBase 8 | { 9 | public String Sha { get; set; } 10 | public String ReferenceName { get; set; } 11 | public String CommitMessageShort { get; set; } 12 | public String CommitMessage { get; set; } 13 | public Signature Author { get; set; } 14 | public Signature Committer { get; set; } 15 | public String[] Parents { get; set; } 16 | public IEnumerable Changes { get; set; } 17 | public PathBarModel PathBar { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Views/User/_List_Search.cshtml: -------------------------------------------------------------------------------- 1 | @using NewLife.Cube 2 | @using NewLife.GitCandy.Entity 3 | @using NewLife; 4 | @using NewLife.Web; 5 | @using XCode; 6 | @{ 7 | var fact = ViewBag.Factory as IEntityFactory; 8 | var page = ViewBag.Page as Pager; 9 | 10 | var dic = new Dictionary(); 11 | dic.Add(1, "团队"); 12 | dic.Add(0, "个人"); 13 | } 14 | @await Html.PartialAsync("_Enable") 15 |
16 | 17 | @Html.ForDropDownList("isTeam", dic, page["isTeam"], "全部", true) 18 |
19 | @*@await Html.PartialAsync("_SelectDepartment", "departmentId")*@ 20 | @await Html.PartialAsync("_DateRange") 21 | -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Views/Repository/_List_Search.cshtml: -------------------------------------------------------------------------------- 1 | @using NewLife.Cube 2 | @using NewLife.GitCandy.Entity 3 | @using NewLife; 4 | @using NewLife.Web; 5 | @using XCode; 6 | @{ 7 | var fact = ViewBag.Factory as IEntityFactory; 8 | var page = ViewBag.Page as Pager; 9 | 10 | var dic = new Dictionary(); 11 | dic.Add(1, "私有"); 12 | dic.Add(0, "公开"); 13 | } 14 | @await Html.PartialAsync("_Enable") 15 |
16 | 17 | @Html.ForDropDownList("isPrivate", dic, page["isPrivate"], "全部", true) 18 |
19 | @*@await Html.PartialAsync("_SelectDepartment", "departmentId")*@ 20 | @await Html.PartialAsync("_DateRange") 21 | -------------------------------------------------------------------------------- /GitCandy.Web/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using NewLife; 4 | using NewLife.Cube; 5 | 6 | namespace GitCandy.Web.Controllers; 7 | 8 | public class HomeController : CandyControllerBase 9 | { 10 | public ActionResult Index() => RedirectToStartPage(); 11 | 12 | [AllowAnonymous] 13 | public ActionResult About() => View(); 14 | 15 | [AllowAnonymous] 16 | public ActionResult Language(String lang) 17 | { 18 | Response.Cookies.Append("Lang", lang); 19 | 20 | //Session["Culture"] = null; 21 | 22 | var url = Request.GetReferer(); 23 | return url.IsNullOrEmpty() ? RedirectToStartPage() : Redirect(url); 24 | } 25 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/TreeModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace GitCandy.Models 5 | { 6 | public class TreeModel : RepositoryModelBase 7 | { 8 | public String ReferenceName { get; set; } 9 | public String Path { get; set; } 10 | public CommitModel Commit { get; set; } 11 | public IEnumerable Entries { get; set; } 12 | public TreeEntryModel Readme { get; set; } 13 | public Boolean IsRoot => String.IsNullOrEmpty(Path) || Path == "\\" || Path == "/"; 14 | public RepositoryScope Scope { get; set; } 15 | public GitUrl[] GitUrls { get; set; } 16 | public String Description { get; set; } 17 | public PathBarModel PathBar { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/CollaborationModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Models 4 | { 5 | public class CollaborationModel : RepositoryModelBase 6 | { 7 | public UserRole[] Users { get; set; } 8 | public TeamRole[] Teams { get; set; } 9 | 10 | public class UserRole 11 | { 12 | public String Name { get; set; } 13 | public Boolean AllowRead { get; set; } 14 | public Boolean AllowWrite { get; set; } 15 | public Boolean IsOwner { get; set; } 16 | } 17 | 18 | public class TeamRole 19 | { 20 | public String Name { get; set; } 21 | public Boolean AllowRead { get; set; } 22 | public Boolean AllowWrite { get; set; } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/_BlobPreview.cshtml: -------------------------------------------------------------------------------- 1 | @using GitCandy.Models 2 | @model GitCandy.Models.TreeEntryModel 3 | 4 | @if (Model.BlobType == BlobType.Text) 5 | { 6 |
@Model.TextContent
7 | } 8 | else if (Model.BlobType == BlobType.MarkDown) 9 | { 10 |
@Model.TextContent
11 | } 12 | else if (Model.BlobType == BlobType.Image) 13 | { 14 | @Model.Name 15 | } 16 | else // Binary 17 | { 18 |
19 | @SR.Repository_BinaryFileWrods
20 | @Html.ActionLink(SR.Repository_Raw, "Raw", Html.OverRoute(new { path = (Model.ReferenceName ?? Model.Commit.Sha) + "/" + Model.Path })) 21 |
22 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/TreeEntryModel.cs: -------------------------------------------------------------------------------- 1 | using LibGit2Sharp; 2 | using System.Text; 3 | using System; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class TreeEntryModel : RepositoryModelBase 8 | { 9 | //public String Name { get; set; } 10 | public String Path { get; set; } 11 | public String ReferenceName { get; set; } 12 | public CommitModel Commit { get; set; } 13 | public String Sha { get; set; } 14 | public TreeEntryTargetType EntryType { get; set; } 15 | public Byte[] RawData { get; set; } 16 | public String SizeString { get; set; } 17 | public String TextContent { get; set; } 18 | public String TextBrush { get; set; } 19 | public BlobType BlobType { get; set; } 20 | public Encoding BlobEncoding { get; set; } 21 | public PathBarModel PathBar { get; set; } 22 | } 23 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Team/Delete.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Team_DeleteTitle, Model)); 3 | } 4 | 5 |
6 |
7 |

@SR.Shared_Conform

8 |
9 |
10 | @String.Format(SR.Team_DeletionWords, Model) 11 |
12 |
13 | 14 | @Html.ValidationSummary(true, "", new { @class = "alert alert-error" }) 15 | 16 |
17 |
18 | @Html.ActionLink(SR.Shared_Back, "Detail", "Team", new { name = Model }, new { @class = "btn btn-default" }) 19 |
20 |
21 | @Html.ActionLink(SR.Shared_Delete, "Delete", "Team", new { name = Model, Conform = "Yes" }, new { @class = "btn btn-danger" }) 22 |
23 |
24 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Delete.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Account_DeleteTitle, Model)); 3 | } 4 | 5 |
6 |
7 |

@SR.Shared_Conform

8 |
9 |
10 | @String.Format(SR.Account_DeletionWords, Model) 11 |
12 |
13 | 14 | @Html.ValidationSummary(true, "", new { @class = "alert alert-error" }) 15 | 16 |
17 |
18 | @Html.ActionLink(SR.Shared_Back, "Detail", "Account", new { name = Model }, new { @class = "btn btn-default" }) 19 |
20 |
21 | @Html.ActionLink(SR.Shared_Delete, "Delete", "Account", new { name = Model, Conform = "Yes" }, new { @class = "btn btn-danger" }) 22 |
23 |
24 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Delete.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_DeleteTitle, Model)); 3 | } 4 | 5 |
6 |
7 |

@SR.Shared_Conform

8 |
9 |
10 | @String.Format(SR.Repository_DeletionWords, Model) 11 |
12 |
13 | 14 | @Html.ValidationSummary(true, "", new { @class = "alert alert-error" }) 15 | 16 |
17 |
18 | @Html.ActionLink(SR.Shared_Back, "Detail", "Repository", new { id = Model }, new { @class = "btn btn-default" }) 19 |
20 |
21 | @Html.ActionLink(SR.Shared_Delete, "Delete", "Repository", new { id = Model, Conform = "Yes" }, new { @class = "btn btn-danger" }) 22 |
23 |
24 | -------------------------------------------------------------------------------- /GitCandy.Web/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | true 8 | false 9 | true 10 | Release 11 | Any CPU 12 | FileSystem 13 | ..\Bin\publish\ 14 | FileSystem 15 | <_TargetId>Folder 16 | 17 | net7.0 18 | win-x64 19 | d1e514b2-2a79-4101-bb0d-ff2139ffd1ec 20 | false 21 | 22 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/RepositoryStatisticsModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GitCandy.Models 4 | { 5 | public class RepositoryStatisticsModel 6 | { 7 | public Statistics Default { get; set; } 8 | public Statistics Current { get; set; } 9 | public Int64 RepositorySize { get; set; } 10 | 11 | [Serializable] 12 | public class Statistics 13 | { 14 | public String Branch { get; set; } 15 | public Int32 NumberOfFiles { get; set; } 16 | public Int32 NumberOfCommits { get; set; } 17 | public Int64 SizeOfSource { get; set; } 18 | public Int32 NumberOfContributors { get; set; } 19 | public ContributorCommits[] OrderedCommits { get; set; } 20 | } 21 | 22 | [Serializable] 23 | public class ContributorCommits 24 | { 25 | public String Author { get; set; } 26 | public Int32 CommitsCount { get; set; } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /GitCandy.Web/Base/UserUrlConstraint.cs: -------------------------------------------------------------------------------- 1 | using NewLife; 2 | using NewLife.Caching; 3 | using NewLife.GitCandy.Entity; 4 | 5 | namespace GitCandy.Web.Base; 6 | 7 | class UserUrlConstraint : IRouteConstraint 8 | { 9 | public Boolean? IsTeam { get; set; } 10 | 11 | public Boolean Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection) 12 | { 13 | var name = values[routeKey] + ""; 14 | if (name.IsNullOrEmpty()) return false; 15 | 16 | var m = Match(name); 17 | 18 | if (IsTeam == null) return m != null; 19 | 20 | return IsTeam == m; 21 | } 22 | 23 | private static ICache _cache = new MemoryCache(); 24 | private static Boolean? Match(String name) 25 | { 26 | var user = _cache.Get(name); 27 | if (user != null) return user.IsTeam; 28 | 29 | user = User.FindByName(name); 30 | 31 | _cache.Set(name, user, 10 * 60); 32 | 33 | return user?.IsTeam; 34 | } 35 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/_GitUrlButton.cshtml: -------------------------------------------------------------------------------- 1 | @using GitCandy.Models 2 | @using GitCandy.Web.App_GlobalResources 3 | @model GitUrl[] 4 | 5 | @{ 6 | var first = Model.First(); 7 | } 8 | 9 |
10 |
11 |
12 | 13 | 19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/_PathBar.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.PathBarModel 2 | @{ 3 | var branch = (Model.ReferenceName ?? Model.ReferenceSha); 4 | } 5 | 6 | 37 | -------------------------------------------------------------------------------- /GitCandy/Base/Profiler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Web; 4 | 5 | namespace GitCandy.Base 6 | { 7 | public class Profiler 8 | { 9 | const String CacheKey = "GitCandyProfiler"; 10 | 11 | private readonly Stopwatch _sw; 12 | 13 | private Profiler() 14 | { 15 | _sw = new Stopwatch(); 16 | _sw.Start(); 17 | } 18 | 19 | public static Profiler Current 20 | { 21 | get 22 | { 23 | var context = HttpContext.Current; 24 | if (context == null) return null; 25 | 26 | return context.Items[CacheKey] as Profiler; 27 | } 28 | private set 29 | { 30 | var context = HttpContext.Current; 31 | if (context == null) return; 32 | 33 | context.Items[CacheKey] = value; 34 | } 35 | } 36 | 37 | public TimeSpan Elapsed => _sw.Elapsed; 38 | 39 | public static void Start() 40 | { 41 | Current = new Profiler(); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/_Pager.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | var action = ViewContext.RouteData.Values["Action"] as String; 3 | var pager = ViewBag.Pager as GitCandy.Base.Pager; 4 | } 5 |
6 |
    7 | @if (pager.HasPreviousPage) 8 | { 9 |
  • @Html.ActionLink("<<", action, new { page = pager.FirstPageIndex })
  • 10 |
  • @Html.ActionLink("<", action, new { page = pager.PreviousPageIndex })
  • 11 | } 12 | 13 | @foreach (int page in pager) 14 | { 15 | if (page == pager.CurrentPageIndex) 16 | { 17 |
  • @(page)
  • 18 | } 19 | else 20 | { 21 |
  • @Html.ActionLink(page.ToString(), action, new { page })
  • 22 | } 23 | } 24 | 25 | @if (pager.HasNextPage) 26 | { 27 |
  • @Html.ActionLink(">", action, new { page = pager.NextPageIndex })
  • 28 |
  • @Html.ActionLink(">>", action, new { page = pager.LastPageIndex })
  • 29 | } 30 |
31 |
32 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | 新生命开发团队 7 | ©2002-2025 NewLife 8 | 1.2 9 | $([System.DateTime]::Now.ToString(`yyyy.MMdd`)) 10 | $(VersionPrefix).$(VersionSuffix) 11 | $(Version) 12 | 1.0.* 13 | false 14 | ..\Bin\Test 15 | false 16 | enable 17 | latest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /GitCandy.Web/Git/RepositorySizeAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.IO; 4 | using GitCandy.Git.Cache; 5 | using LibGit2Sharp; 6 | 7 | namespace GitCandy.Git 8 | { 9 | public class RepositorySizeAccessor : GitCacheAccessor 10 | { 11 | private String key; 12 | 13 | public RepositorySizeAccessor(String repoId, Repository repo, String key) 14 | : base(repoId, repo) 15 | { 16 | Contract.Requires(key != null); 17 | 18 | this.key = key; 19 | } 20 | 21 | public override Boolean IsAsync => false; 22 | 23 | protected override String GetCacheKey() => GetCacheKey(key); 24 | 25 | protected override void Init() => _result = 0; 26 | 27 | protected override void Calculate() 28 | { 29 | var info = new DirectoryInfo(this.repoPath); 30 | foreach (var file in info.GetFiles("*", SearchOption.AllDirectories)) 31 | { 32 | _result += file.Length; 33 | } 34 | _resultDone = true; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Home_AboutTitle); 3 | } 4 | 5 |

@SR.Home_AboutTitle

6 | 7 |
8 | @Html.Raw(SR.Home_AboutDescription) 9 | * [ASP.NET MVC](http://aspnetwebstack.codeplex.com/) @@ [Apache License 2.0](http://aspnetwebstack.codeplex.com/license) 10 | * [Bootstrap](http://github.com/twbs/bootstrap) @@ [MIT License](http://github.com/twbs/bootstrap/blob/master/LICENSE) 11 | * [Bootstrap-switch](http://github.com/nostalgiaz/bootstrap-switch) @@ [Apache License 2.0](http://github.com/nostalgiaz/bootstrap-switch/blob/master/LICENSE) 12 | * [Highlight.js](http://github.com/isagalaev/highlight.js) @@ [New BSD License](http://github.com/isagalaev/highlight.js/blob/master/LICENSE) 13 | * [jQuery](http://github.com/jquery/jquery) @@ [MIT License](http://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) 14 | * [LibGit2Sharp](http://github.com/libgit2/libgit2sharp) @@ [MIT License](http://github.com/libgit2/libgit2sharp/blob/master/LICENSE.md) 15 | * [marked](http://github.com/chjj/marked) @@ [MIT License](http://github.com/chjj/marked/blob/master/LICENSE) 16 |
-------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2016 Aimeast 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /GitCandy.Web/Base/RawResult.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Net.Mime; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace GitCandy.Web.Base; 6 | 7 | public class RawResult : ActionResult 8 | { 9 | public RawResult(Byte[] contents, String contentType = "text/plain", String fileDownloadName = null) 10 | { 11 | Contents = contents ?? throw new ArgumentNullException("contents"); 12 | ContentType = contentType; 13 | FileDownloadName = fileDownloadName; 14 | } 15 | 16 | public Byte[] Contents { get; private set; } 17 | public String ContentType { get; private set; } 18 | public String FileDownloadName { get; private set; } 19 | 20 | public override void ExecuteResult(ActionContext context) 21 | { 22 | if (context == null) 23 | throw new ArgumentNullException("context"); 24 | 25 | var response = context.HttpContext.Response; 26 | if (!String.IsNullOrEmpty(FileDownloadName)) 27 | response.Headers.ContentDisposition = new ContentDisposition { FileName = FileDownloadName }.ToString(); 28 | 29 | response.ContentType = ContentType; 30 | response.BodyWriter.Write(Contents); 31 | } 32 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.LoginModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Account_LoginTitle); 5 | var showadmin = ViewBag.ShowAdmin as String; 6 | var adminstr = String.Format("默认管理员 {0}/{0}", showadmin); 7 | } 8 | 9 |

@SR.Account_LoginTitle

10 | 11 | @using (Html.BeginForm("Login", "Account", new { ViewBag.ReturnUrl }, FormMethod.Post)) 12 | { 13 |
14 |
@Html.DisplayNameFor(s => s.ID)
15 |
@Html.TextBoxFor(s => s.ID, new { @class = "form-control" })
16 | 17 |
@Html.DisplayNameFor(s => s.Password)
18 |
@Html.PasswordFor(s => s.Password, new { @class = "form-control" })
19 | 20 |
21 |
@Html.ValidationSummary(true, SR.Account_LoginFailed, new { @class = "alert alert-dismissable alert-danger" })
22 | 23 |
24 |
25 | 26 | @Html.ActionLink(SR.Account_ForgotPassword, "Forgot") 27 | @if (!showadmin.IsNullOrEmpty()) 28 | { 29 | @adminstr 30 | } 31 |
32 |
33 | } 34 | -------------------------------------------------------------------------------- /GitCandy.Web/Extensions/CommitLogExtension.cs: -------------------------------------------------------------------------------- 1 | using LibGit2Sharp; 2 | using Microsoft.AspNetCore.Html; 3 | using Microsoft.AspNetCore.Mvc.Rendering; 4 | using NewLife.GitCandy.Entity; 5 | 6 | namespace GitCandy.Web.Extensions; 7 | 8 | public static class CommitLogExtension 9 | { 10 | public static IEnumerable PathFilter(this IEnumerable log, String path) 11 | { 12 | if (String.IsNullOrEmpty(path)) 13 | return log; 14 | 15 | return log.Where(s => 16 | { 17 | var pathEntry = s[path]; 18 | var parent = s.Parents.FirstOrDefault(); 19 | if (parent == null) 20 | return pathEntry != null; 21 | 22 | var parentPathEntry = parent[path]; 23 | if (pathEntry == null && parentPathEntry == null) 24 | return false; 25 | if (pathEntry != null && parentPathEntry != null) 26 | return pathEntry.Target.Sha != parentPathEntry.Target.Sha; 27 | return true; 28 | }); 29 | } 30 | 31 | public static IHtmlContent Link(this IHtmlHelper html, Signature sign) 32 | { 33 | var user = User.FindByName(sign.Name) ?? User.FindByEmail(sign.Email); 34 | if (user != null) 35 | return html.ActionLink(user + "", "Detail", "Account", new { name = user.Name }, new { target = "_blank" }); 36 | else 37 | return new HtmlString(sign.Name); 38 | } 39 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Team/Detail.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TeamModel 2 | @using GitCandy.Web.App_GlobalResources 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @{ 6 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Team_DetailTitle, Model.Name)); 7 | var token = ViewBag.Token as IManageUser; 8 | } 9 | 10 |

@String.Format(SR.Team_DetailTitle, Model.Name)

11 | 12 | 13 | @if (Model != null) 14 | { 15 |
16 | 17 |
@Html.DisplayNameFor(s => s.Name)
18 |
@Model.Name
19 | 20 |
@Html.DisplayNameFor(s => s.Nickname)
21 |
@Model.Nickname
22 | 23 |
@Html.DisplayNameFor(s => s.Members)
24 |
@Html.DisplayFor(s => s.Members)
25 | 26 |
@Html.DisplayNameFor(s => s.Repositories)
27 |
@Html.DisplayFor(s => s.Repositories)
28 | 29 |
@Html.DisplayNameFor(s => s.Description)
30 |
@Model.Description
31 | 32 | @if (token != null && token.IsAdmin()) 33 | { 34 |
35 |
36 | @Html.ActionLink(SR.Shared_Edit, "Edit", new { name = Model.Name }, new { @class = "btn btn-primary" }) 37 | @Html.ActionLink(SR.Team_Members, "Users", new { name = Model.Name }, new { @class = "btn btn-info" }) 38 |
39 | } 40 |
41 | } 42 | -------------------------------------------------------------------------------- /GitCandy.Web/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using GitCandy.Base; 2 | using GitCandy.Web.Extensions; 3 | using Microsoft.AspNetCore.Mvc; 4 | using NewLife; 5 | using NewLife.Data; 6 | using UserX = NewLife.GitCandy.Entity.User; 7 | 8 | namespace GitCandy.Web.Controllers; 9 | 10 | public class AccountController : CandyControllerBase 11 | { 12 | public ActionResult Index(String query, Int32? page) 13 | { 14 | if (!Token.IsAdmin()) return Forbid(); 15 | 16 | var model = MembershipService.GetUserList(query, page ?? 1, GitSetting.Current.PageSize); 17 | 18 | ViewBag.Pager = Pager.Items(model.ItemCount) 19 | .PerPage(GitSetting.Current.PageSize) 20 | .Move(model.CurrentPage) 21 | .Segment(5) 22 | .Center(); 23 | 24 | return View(model); 25 | } 26 | 27 | public ActionResult Detail(String name) 28 | { 29 | if (name.IsNullOrEmpty()) name = Token?.Name; 30 | 31 | var model = MembershipService.GetUserModel(name, true, Token?.Name); 32 | if (model == null) return NotFound(name); 33 | 34 | return View(model); 35 | } 36 | 37 | [HttpPost] 38 | public JsonResult Search(String query) 39 | { 40 | var p = new PageParameter 41 | { 42 | PageSize = 20 43 | }; 44 | 45 | var list = UserX.SearchUser(query, p); 46 | var ns = list.ToList().Select(e => e.Name).ToArray(); 47 | 48 | return Json(ns); 49 | } 50 | } -------------------------------------------------------------------------------- /GitCandy.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Urls": "http://*:7000;https://*:7001", 11 | //"StarServer": "http://star.newlifex.com:6600", 12 | "ConnectionStrings": { 13 | "GitCandy": "Data Source=..\\Data\\GitCandy.db;Provider=SQLite", 14 | "Membership": "Data Source=..\\Data\\Membership.db;Provider=SQLite", 15 | 16 | // 各种数据库连接字符串模版,连接名GitCandy对应Model.xml中的ConnName 17 | //"GitCandy": "Server=.;Port=3306;Database=gitCandy;Uid=root;Pwd=root;Provider=MySql", 18 | //"GitCandy": "Data Source=.;Initial Catalog=gitCandy;user=sa;password=sa;Provider=SqlServer", 19 | //"GitCandy": "Server=.;Database=gitCandy;Uid=root;Pwd=root;Provider=PostgreSql", 20 | //"GitCandy": "Data Source=Tcp://127.0.0.1/ORCL;User Id=scott;Password=tiger;Provider=Oracle" 21 | 22 | //"Membership": "Server=.;Port=3306;Database=gitCandy;Uid=root;Pwd=root;Provider=MySql", 23 | //"Membership": "Data Source=.;Initial Catalog=gitCandy;user=sa;password=sa;Provider=SqlServer", 24 | //"Membership": "Server=.;Database=gitCandy;Uid=root;Pwd=root;Provider=PostgreSql", 25 | //"Membership": "Data Source=Tcp://127.0.0.1/ORCL;User Id=scott;Password=tiger;Provider=Oracle" 26 | 27 | // 魔方审计日志使用Membership的连接字符串 28 | "Log": "MapTo=Membership", 29 | "Cube": "MapTo=Membership" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Change.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.ChangePasswordModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Account_ChangePasswordTitle); 5 | } 6 | 7 |

@SR.Account_ChangePasswordTitle

8 | 9 | @using (Html.BeginForm("Change", "Account", FormMethod.Post)) 10 | { 11 |
12 |
@Html.DisplayNameFor(s => s.OldPassword)
13 |
@Html.PasswordFor(s => s.OldPassword, new { @class = "form-control" })
14 |
15 | 16 | @Html.ValidationMessageFor(s => s.OldPassword) 17 | 18 |
19 | 20 |
@Html.DisplayNameFor(s => s.NewPassword)
21 |
@Html.PasswordFor(s => s.NewPassword, new { @class = "form-control" })
22 |
23 | 24 | @Html.ValidationMessageFor(s => s.NewPassword) 25 | 26 |
27 | 28 |
@Html.DisplayNameFor(s => s.ConformPassword)
29 |
@Html.PasswordFor(s => s.ConformPassword, new { @class = "form-control" })
30 |
31 | 32 | @Html.ValidationMessageFor(s => s.ConformPassword) 33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | } 42 | -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/RepositoryController.cs: -------------------------------------------------------------------------------- 1 | using NewLife; 2 | using NewLife.Cube; 3 | using NewLife.Cube.ViewModels; 4 | using NewLife.GitCandy.Entity; 5 | using NewLife.Web; 6 | 7 | namespace GitCandy.Web.Areas.GitCandy.Controllers; 8 | 9 | [GitCandyArea] 10 | public class RepositoryController : EntityController 11 | { 12 | static RepositoryController() 13 | { 14 | LogOnChange = true; 15 | 16 | ListFields.RemoveCreateField(); 17 | ListFields.RemoveUpdateField(); 18 | ListFields.RemoveRemarkField(); 19 | 20 | { 21 | var df = ListFields.AddDataField("UserRepositorys", "Commits") as ListField; 22 | df.DisplayName = "关联团队/用户"; 23 | df.Url = "/GitCandy/UserRepository?repositoryId={ID}"; 24 | } 25 | } 26 | 27 | protected override IEnumerable Search(Pager p) 28 | { 29 | var id = p["Id"].ToInt(-1); 30 | if (id > 0) 31 | { 32 | var entity = Repository.FindByKey(id); 33 | if (entity != null) return new[] { entity }; 34 | } 35 | 36 | var ownerId = p["ownerId"].ToInt(-1); 37 | var userId = p["userId"].ToInt(-1); 38 | var enable = p["enable"]?.ToBoolean(); 39 | var isPrivate = p["isPrivate"]?.ToBoolean(); 40 | 41 | var start = p["dtStart"].ToDateTime(); 42 | var end = p["dtEnd"].ToDateTime(); 43 | 44 | return Repository.Search(ownerId, userId, enable, isPrivate, start, end, p["q"], p); 45 | } 46 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Tags.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TagsModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_TagsTitle, Model.Name)); 5 | } 6 | 7 |

8 | @String.Format(SR.Repository_TagsTitle, "") @await Html.PartialAsync("_RepositoryLink", Model) 9 |

10 | 11 |
12 | @if (Model.HasTags) 13 | { 14 | foreach (var tag in Model.Tags) 15 | { 16 |
17 |
18 | @await Html.PartialAsync("_ZipButton", tag.ReferenceName) 19 | @if (Model.CanDelete) 20 | { @SR.Shared_Delete} 21 |
22 |
@Html.ActionLink(tag.ReferenceName, "Tree", new { path = tag.ReferenceName ?? tag.Sha })
23 |
@tag.When.LocalDateTime.ToFullString()
24 |
@tag.MessageShort
25 |
@Html.ActionLink(tag.Sha, "Commit", new { path = tag.ReferenceName ?? tag.Sha })
26 |
27 | } 28 | } 29 | else 30 | { 31 |
@SR.Repository_NoTags
32 | } 33 |
34 | 35 | 41 | -------------------------------------------------------------------------------- /Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using LibGit2Sharp; 5 | using NewLife.Log; 6 | 7 | namespace Test 8 | { 9 | class Program 10 | { 11 | static void Main(String[] args) 12 | { 13 | XTrace.UseConsole(); 14 | 15 | try 16 | { 17 | Test1(); 18 | } 19 | catch (Exception ex) 20 | { 21 | XTrace.WriteException(ex); 22 | } 23 | 24 | Console.WriteLine("OK"); 25 | Console.ReadLine(); 26 | } 27 | 28 | static void Test1() 29 | { 30 | //var remoteUrl = "https://gitee.com/NewLifeX/NewLife.Cube"; 31 | var remoteUrl = "https://gitee.com/NewLifeX/GitCandy"; 32 | var xx = "xx".GetFullPath(); 33 | //if (Directory.Exists(xx)) Directory.Delete(xx, true); 34 | var p = xx; 35 | if (!Directory.Exists(xx)) p = Repository.Init(xx, true); 36 | using (var repo = new Repository(p)) 37 | { 38 | repo.Network.Remotes.Add("origin", remoteUrl, "+refs/*:refs/*"); 39 | 40 | //var refs = repo.Network.ListReferences("origin").ToList(); 41 | //XTrace.WriteLine("发现分支:{0}", refs.Select(e => e.TargetIdentifier)); 42 | 43 | //repo.Network.Fetch("origin", new[] { "master" }); 44 | repo.Network.Fetch("origin", new string[0]); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Team/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TeamListModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Team_ListTitle); 5 | } 6 | 7 |

@SR.Team_ListTitle

8 | 9 | @using (Html.BeginForm("Index", "Team", FormMethod.Get)) 10 | { 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | } 22 | 23 |
24 |
@String.Format(SR.Team_TeamsFound, Model.ItemCount)
25 |
@Html.ActionLink(SR.Shared_Create, "Create", routeValues: null, new { @class = "btn btn-primary" })
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | @foreach (var item in Model.Teams) 37 | { 38 | 39 | 40 | 41 | 42 | 43 | } 44 | 45 |
@SR.Team_Name@SR.Team_Name@SR.Team_Description
@Html.ActionLink(item.Name, "Detail", new { name = item.Name })@item.Nickname@item.Description
46 | @await Html.PartialAsync("_Pager") -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/Content/highlight.css: -------------------------------------------------------------------------------- 1 | /* 2 | Docco style used in http://jashkenas.github.com/docco/ converted by Simon Madine (@thingsinjars) 3 | */ 4 | 5 | .hljs { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 0.5em; 9 | color: #000; 10 | background: #f8f8ff; 11 | } 12 | 13 | .hljs-comment, 14 | .hljs-quote { 15 | color: #408080; 16 | font-style: italic; 17 | } 18 | 19 | .hljs-keyword, 20 | .hljs-selector-tag, 21 | .hljs-literal, 22 | .hljs-subst { 23 | color: #954121; 24 | } 25 | 26 | .hljs-number { 27 | color: #40a070; 28 | } 29 | 30 | .hljs-string, 31 | .hljs-doctag { 32 | color: #219161; 33 | } 34 | 35 | .hljs-selector-id, 36 | .hljs-selector-class, 37 | .hljs-section, 38 | .hljs-type { 39 | color: #19469d; 40 | } 41 | 42 | .hljs-params { 43 | color: #00f; 44 | } 45 | 46 | .hljs-title { 47 | color: #458; 48 | font-weight: bold; 49 | } 50 | 51 | .hljs-tag, 52 | .hljs-name, 53 | .hljs-attribute { 54 | color: #000080; 55 | font-weight: normal; 56 | } 57 | 58 | .hljs-variable, 59 | .hljs-template-variable { 60 | color: #008080; 61 | } 62 | 63 | .hljs-regexp, 64 | .hljs-link { 65 | color: #b68; 66 | } 67 | 68 | .hljs-symbol, 69 | .hljs-bullet { 70 | color: #990073; 71 | } 72 | 73 | .hljs-built_in, 74 | .hljs-builtin-name { 75 | color: #0086b3; 76 | } 77 | 78 | .hljs-meta { 79 | color: #999; 80 | font-weight: bold; 81 | } 82 | 83 | .hljs-deletion { 84 | background: #fdd; 85 | } 86 | 87 | .hljs-addition { 88 | background: #dfd; 89 | } 90 | 91 | .hljs-emphasis { 92 | font-style: italic; 93 | } 94 | 95 | .hljs-strong { 96 | font-weight: bold; 97 | } 98 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/ChangePasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using GitCandy.Web.App_GlobalResources; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class ChangePasswordModel 8 | { 9 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 10 | [StringLength(100, MinimumLength =5, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 11 | [DataType(DataType.Password)] 12 | [Display(ResourceType = typeof(SR), Name = "Account_OldPassword")] 13 | public String OldPassword { get; set; } 14 | 15 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 16 | [StringLength(100, MinimumLength = 6, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 17 | [DataType(DataType.Password)] 18 | [Display(ResourceType = typeof(SR), Name = "Account_NewPassword")] 19 | public String NewPassword { get; set; } 20 | 21 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 22 | [StringLength(100, MinimumLength = 6, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 23 | [Compare("NewPassword", ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Compare")] 24 | [DataType(DataType.Password)] 25 | [Display(ResourceType = typeof(SR), Name = "Account_ConformPassword")] 26 | public String ConformPassword { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GitCandy.Web/Git/CommitsAccessor.cs: -------------------------------------------------------------------------------- 1 | using GitCandy.Git.Cache; 2 | using GitCandy.Web.Extensions; 3 | using LibGit2Sharp; 4 | 5 | namespace GitCandy.Git; 6 | 7 | public class CommitsAccessor(String repoId, Repository repo, Commit commit, String path, Int32 page, Int32 pageSize) : GitCacheAccessor(repoId, repo) 8 | { 9 | public override Boolean IsAsync => false; 10 | 11 | protected override String GetCacheKey() => GetCacheKey(commit.Sha, path, page, pageSize); 12 | 13 | protected override void Init() => _result = []; 14 | 15 | protected override void Calculate() 16 | { 17 | using (var repo = new Repository(repoPath)) 18 | { 19 | _result = repo.Commits 20 | .QueryBy(new CommitFilter { IncludeReachableFrom = commit, SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time }) 21 | .PathFilter(path) 22 | .Skip((page - 1) * pageSize) 23 | .Take(pageSize) 24 | .Select(s => new RevisionSummaryCacheItem 25 | { 26 | CommitSha = s.Sha, 27 | MessageShort = s.MessageShort.RepetitionIfEmpty(GitService.UnknowString), 28 | AuthorName = s.Author.Name, 29 | AuthorEmail = s.Author.Email, 30 | AuthorWhen = s.Author.When, 31 | CommitterName = s.Committer.Name, 32 | CommitterEmail = s.Committer.Email, 33 | CommitterWhen = s.Committer.When, 34 | }) 35 | .ToArray(); 36 | } 37 | _resultDone = true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Compare.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.CompareModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_CompareTitle, Model.Name)); 5 | } 6 | 7 |

8 | @await Html.PartialAsync("_RepositoryLink", Model) 9 |

10 | 11 |
12 |
13 | @await Html.PartialAsync("_BranchSelector", Model.BaseBranchSelector) 14 |
15 |
16 | @await Html.PartialAsync("_BranchSelector", Model.CompareBranchSelector) 17 |
18 |
19 | 20 |
21 |
22 | 23 | @if (Model.Walks == null || Model.Walks.Length == 0) 24 | { 25 | @SR.Repository_CompareNothing 26 | } 27 | else 28 | { 29 |
30 | 31 | 32 | @foreach (var commit in Model.Walks) 33 | { 34 | 35 | 36 | 37 | 38 | 39 | } 40 | 41 |
@commit.Committer.Name.ShortString(20)@commit.Committer.When.LocalDateTime.ToFullString()@Html.ActionLink(commit.CommitMessageShort.ShortString(80), "Commit", Html.OverRoute(new { path = commit.Sha }))
42 |
43 | 44 | @await Html.PartialAsync("_Diff", Model.CompareResult) 45 | } 46 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Team/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TeamModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Team_CreationTitle); 5 | } 6 | 7 |

@SR.Team_CreationTitle

8 | 9 | @using (Html.BeginForm("Create", "Team", FormMethod.Post)) 10 | { 11 |
12 |
@Html.DisplayNameFor(s => s.Name)
13 |
@Html.TextBoxFor(s => s.Name, new { @class = "form-control" })
14 |
15 | 16 | @Html.ValidationMessageFor(s => s.Name) 17 | 18 |
19 | 20 |
@Html.DisplayNameFor(s => s.Nickname)
21 |
@Html.TextBoxFor(s => s.Nickname, new { @class = "form-control" })
22 |
23 | 24 | @Html.ValidationMessageFor(s => s.Nickname) 25 | 26 |
27 | 28 |
@Html.DisplayNameFor(s => s.Description)
29 |
@Html.TextAreaFor(s => s.Description, 4, 0, new { @class = "form-control" })
30 |
31 | 32 | @Html.ValidationMessageFor(s => s.Description) 33 | 34 |
35 | 36 |
37 |
@Html.ValidationSummary(true, SR.Team_CreationUnsuccessfull, new { @class = "alert alert-dismissable alert-danger" })
38 | 39 |
40 |
41 |   42 |   43 |
44 |
45 | } 46 | -------------------------------------------------------------------------------- /GitCandy/GitCandy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | GitCandy.Data 6 | GitCandy.Data 7 | 数据层 8 | 9 | 新生命开发团队 10 | ©2002-2025 NewLife 11 | 2.0 12 | $([System.DateTime]::Now.ToString(`yyyy.MMdd`)) 13 | $(VersionPrefix).$(VersionSuffix) 14 | $(Version) 15 | $(VersionPrefix).* 16 | false 17 | latest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Detail.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.UserModel 2 | @using GitCandy.Web.App_GlobalResources 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @{ 6 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Account_DetailTitle, Model.Name)); 7 | var token = ViewBag.Token as IManageUser; 8 | } 9 |

@String.Format(SR.Account_DetailTitle, Model.Name)

10 | 11 | @if (Model != null) 12 | { 13 |
14 | 15 |
@Html.DisplayNameFor(s => s.Name)
16 |
@Model.Name
17 | 18 |
@Html.DisplayNameFor(s => s.Nickname)
19 |
@Model.Nickname
20 | 21 |
@Html.DisplayNameFor(s => s.Email)
22 |
@Model.Email
23 | 24 |
@Html.DisplayNameFor(s => s.Repositories)
25 |
@Html.DisplayFor(s => s.Repositories)
26 | 27 |
@Html.DisplayNameFor(s => s.Teams)
28 |
@Html.DisplayFor(s => s.Teams)
29 | 30 |
@Html.DisplayNameFor(s => s.Description)
31 |
@Model.Description
32 | 33 | @*@if (token != null && token.IsAdmin()) 34 | { 35 |
@Html.DisplayNameFor(s => s.IsAdmin)
36 |
@Html.DisplayFor(s => s.IsAdmin)
37 | }*@ 38 | 39 | @if (token != null && (token.Name == Model.Name || token.IsAdmin())) 40 | { 41 |
42 |
43 | @Html.ActionLink(SR.Shared_Edit, "Edit", new { Model.Name }, new { @class = "btn btn-primary" }) 44 | @Html.ActionLink(SR.Account_ChangePassword, "Change", new { Model.Name }, new { @class = "btn btn-info" }) 45 |
46 | } 47 |
48 | } 49 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Commit.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.CommitModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_CommitTitle, Model.Name, Model.ReferenceName ?? Model.Sha.ToShortSha(), Model.CommitMessageShort)); 5 | } 6 | 7 |

8 | @await Html.PartialAsync("_RepositoryLink", Model) 9 |

10 | 11 | @await Html.PartialAsync("_PathBar", Model.PathBar) 12 | 13 |
14 |
15 |
@Model.CommitMessage
16 | @Model.Author.Name 17 | @SR.Repository_AuthoredAt 18 | @Model.Author.When.LocalDateTime.ToFullString() 19 | @if (Model.Author != Model.Committer) 20 | { 21 | @Model.Committer.Name 22 | @SR.Repository_CommittedAt 23 | @Model.Committer.When.LocalDateTime.ToFullString() 24 | } 25 |
26 | 27 |
28 |
@Html.ActionLink(Model.Sha.ToShortSha(), "Commit", new { path = Model.Sha })
29 |
@Html.ActionLink(SR.Repository_Tree, "Tree", new { path = Model.Sha })
30 | @Model.Parents.Length @SR.Repository_Parents 31 | @for (var index = 0; index < Model.Parents.Length; index++) 32 | { 33 | if (index > 0) 34 | { 35 | @Html.Raw(" + ") 36 | } 37 | var parent = Model.Parents[index]; 38 | @Html.ActionLink(parent.ToShortSha(), "Commit", new { path = parent }) 39 | } 40 |
41 |
42 | 43 | @await Html.PartialAsync("_Diff", Model) 44 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Commits.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.CommitsModel 2 | @using GitCandy.Web.Extensions 3 | 4 | @{ 5 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_CommitsTitle, Model.Name ?? Model.Sha.ToShortSha(), Model.Path)); 6 | } 7 | 8 |

9 | @SR.Repository_HistoryFor @await Html.PartialAsync("_RepositoryLink", Model) 10 |

11 | 12 | @await Html.PartialAsync("_PathBar", Model.PathBar) 13 | 14 | @foreach (var commit in Model.Commits) 15 | { 16 |
17 |
18 |
19 |
@Html.ActionLink(commit.CommitMessageShort.ShortString(100) ?? "", "Commit", new { path = commit.Sha + "/" + Model.Path })
20 | @Html.Link(commit.Author) 21 | @SR.Repository_AuthoredAt 22 | @commit.Author.When.LocalDateTime.ToFullString() 23 | @if (commit.Author != commit.Committer) 24 | { 25 | @Html.Link(commit.Committer) 26 | @SR.Repository_CommittedAt 27 | @commit.Committer.When.LocalDateTime.ToFullString() 28 | } 29 |
30 |
31 |
@Html.ActionLink(commit.Sha.ToShortSha() ?? "", "Commit", new { path = commit.Sha })
32 |
@Html.ActionLink(SR.Repository_Tree, "Tree", new { path = commit.Sha })
33 |
34 |
35 |
36 | } 37 | @await Html.PartialAsync("_Pager") 38 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.UserListModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Account_ListTitle); 5 | } 6 | 7 |

@SR.Account_ListTitle

8 | 9 | @using (Html.BeginForm("Index", "Account", FormMethod.Get)) 10 | { 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | } 22 | 23 |
24 |
@String.Format(SR.Account_UsersFound, Model.ItemCount)
25 |
@Html.ActionLink(SR.Shared_Create, "Create", routeValues: null, new { @class = "btn btn-primary" })
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | @* 35 | *@ 36 | 37 | 38 | 39 | @foreach (var item in Model.Users) 40 | { 41 | 42 | 43 | 44 | 45 | 46 | @* 47 | *@ 48 | 49 | } 50 | 51 |
@SR.Account_Username@SR.Account_Nickname@SR.Account_Email@SR.Account_IsSystemAdministrator仓库数团队数
@Html.ActionLink(item.Name, "Detail", new { name = item.Name })@item.Nickname@item.Email@item.IsAdmin.ToFlagString(SR.Shared_Yes, SR.Shared_No)@item.Repositories?.Length@item.Teams?.Count
52 | @await Html.PartialAsync("_Pager") -------------------------------------------------------------------------------- /GitCandy.Web/Git/ScopeAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Contracts; 4 | using System.Linq; 5 | using GitCandy.Git.Cache; 6 | using GitCandy.Models; 7 | using GitCandy.Web.Extensions; 8 | using LibGit2Sharp; 9 | 10 | namespace GitCandy.Git 11 | { 12 | public class ScopeAccessor : GitCacheAccessor 13 | { 14 | private readonly Commit commit; 15 | private readonly String path; 16 | private readonly Boolean pathExist; 17 | 18 | public ScopeAccessor(String repoId, Repository repo, Commit commit, String path = "") 19 | : base(repoId, repo) 20 | { 21 | Contract.Requires(commit != null); 22 | 23 | this.commit = commit; 24 | this.path = path; 25 | this.pathExist = commit[path] != null; 26 | } 27 | 28 | protected override String GetCacheKey() => GetCacheKey(commit.Sha, path); 29 | 30 | protected override void Init() 31 | { 32 | _result = new RepositoryScope 33 | { 34 | Commits = 0, 35 | Contributors = 0, 36 | Branches = repo.Branches.Count(), 37 | Tags = repo.Tags.Count(), 38 | }; 39 | } 40 | 41 | protected override void Calculate() 42 | { 43 | using (var repo = new Repository(this.repoPath)) 44 | { 45 | var ancestors = pathExist 46 | ? repo.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = commit }).PathFilter(path) 47 | : repo.Commits.QueryBy(new CommitFilter { IncludeReachableFrom = commit }); 48 | 49 | var set = new HashSet(); 50 | foreach (var ancestor in ancestors) 51 | { 52 | _result.Commits++; 53 | if (set.Add(ancestor.Author.ToString())) 54 | _result.Contributors++; 55 | } 56 | } 57 | _resultDone = true; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Team/Edit.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TeamModel 2 | @using GitCandy.Web.App_GlobalResources 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @{ 6 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Team_EditTitle, Model.Name)); 7 | var token = ViewBag.Token as IManageUser; 8 | } 9 | 10 |

@String.Format(SR.Team_EditTitle, Model.Name)

11 | 12 | 13 | @using (Html.BeginForm("Edit", "Team", FormMethod.Post)) 14 | { 15 |
16 |
@Html.DisplayNameFor(s => s.Name)
17 |
18 | @Html.HiddenFor(s => s.Name) 19 | @Model.Name 20 |
21 | 22 |
@Html.DisplayNameFor(s => s.Nickname)
23 |
@Html.TextBoxFor(s => s.Nickname, new { @class = "form-control" })
24 |
25 | 26 | @Html.ValidationMessageFor(s => s.Nickname) 27 | 28 |
29 | 30 |
@Html.DisplayNameFor(s => s.Description)
31 |
@Html.TextAreaFor(s => s.Description, 4, 0, new { @class = "form-control" })
32 |
33 | 34 | @Html.ValidationMessageFor(s => s.Description) 35 | 36 |
37 | 38 |
39 |
@Html.ValidationSummary(true, SR.Team_UpdateUnsuccessfull, new { @class = "alert alert-dismissable alert-danger" })
40 | 41 |
42 | @Html.ActionLink(SR.Shared_Back, "Detail", new { Model.Name }, new { @class = "btn btn-default pull-left" }) 43 |
44 |
45 |   46 |   47 | @if (token != null && token.IsAdmin()) 48 | { 49 | @Html.ActionLink(SR.Shared_Delete, "Delete", new { Model.Name }, new { @class = "btn btn-danger" }) 50 | } 51 |
52 |
53 | } 54 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Team/Users.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TeamModel 2 | @using NewLife.Serialization; 3 | 4 | @{ 5 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Team_ChooseUserTitle, Model.Name)); 6 | 7 | var ctrl = "Account"; 8 | //if (HttpRuntime.AppDomainAppVirtualPath != "/") { ctrl = HttpRuntime.AppDomainAppVirtualPath.TrimStart("/") + "/" + ctrl; } 9 | } 10 | 11 |

@String.Format(SR.Team_ChooseUserTitle, Model.Name)

12 | 13 | @if (Model != null) 14 | { 15 |
16 |
17 |
18 |
19 |
20 | 21 | 44 | } 45 | @Html.ActionLink(SR.Shared_Back, "Detail", new { Model.Name }, new { @class = "btn btn-default" }) 46 | -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/GitHistoryController.cs: -------------------------------------------------------------------------------- 1 | using NewLife.GitCandy.Entity; 2 | using NewLife; 3 | using NewLife.Cube; 4 | using NewLife.Cube.Extensions; 5 | using NewLife.Cube.ViewModels; 6 | using NewLife.Web; 7 | using XCode.Membership; 8 | 9 | namespace GitCandy.Web.Areas.GitCandy.Controllers 10 | { 11 | /// Git历史 12 | [Menu(10, true, Icon = "fa-table")] 13 | [GitCandyArea] 14 | public class GitHistoryController : ReadOnlyEntityController 15 | { 16 | static GitHistoryController() 17 | { 18 | //LogOnChange = true; 19 | 20 | //ListFields.RemoveField("Id", "Creator"); 21 | //ListFields.RemoveCreateField(); 22 | ListFields.AddDataField("Remark", "TraceId"); 23 | 24 | //{ 25 | // var df = ListFields.GetField("Code") as ListField; 26 | // df.Url = "?code={Code}"; 27 | //} 28 | //{ 29 | // var df = ListFields.AddListField("devices", null, "Onlines"); 30 | // df.DisplayName = "查看设备"; 31 | // df.Url = "Device?groupId={Id}"; 32 | // df.DataVisible = e => (e as GitHistory).Devices > 0; 33 | //} 34 | //{ 35 | // var df = ListFields.GetField("Kind") as ListField; 36 | // df.GetValue = e => ((Int32)(e as GitHistory).Kind).ToString("X4"); 37 | //} 38 | ListFields.TraceUrl("TraceId"); 39 | } 40 | 41 | /// 高级搜索。列表页查询、导出Excel、导出Json、分享页等使用 42 | /// 分页器。包含分页排序参数,以及Http请求参数 43 | /// 44 | protected override IEnumerable Search(Pager p) 45 | { 46 | var userId = p["userId"].ToInt(-1); 47 | var repoId = p["repoId"].ToInt(-1); 48 | var action = p["action"]; 49 | 50 | var start = p["dtStart"].ToDateTime(); 51 | var end = p["dtEnd"].ToDateTime(); 52 | 53 | return GitHistory.Search(userId, repoId, action, start, end, p["Q"], p); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /GitCandy.Web/GitCandy.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | GitCandy 6 | GitCandy.Web 7 | 糖果代码库 8 | 一个基于.NET 的Git分布式版本控制平台,支持公共和私有代码库。可以不受限制的创建代码库,与你的团队一块协作。 9 | 新生命开发团队 10 | ©2002-2025 NewLife 11 | 3.0 12 | $([System.DateTime]::Now.ToString(`yyyy.MMdd`)) 13 | $(VersionPrefix).$(VersionSuffix) 14 | $(Version) 15 | $(VersionPrefix).* 16 | false 17 | ..\Bin\Web 18 | false 19 | enable 20 | latest 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/_Diff.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.CommitModel 2 | 3 |
4 |
@String.Format(SR.Repository_ChangedSummary, Model.Changes.Count(), Model.Changes.Sum(s => s.LinesAdded), Model.Changes.Sum(s => s.LinesDeleted))
5 | @foreach (var change in Model.Changes) 6 | { 7 | var removed = change.ChangeKind == LibGit2Sharp.ChangeKind.Deleted || change.ChangeKind == LibGit2Sharp.ChangeKind.Ignored; 8 |
9 |
10 |
@change.ChangeKind.ToLocateString() +@change.LinesAdded -@change.LinesDeleted
11 |
12 | @if (change.OldPath != change.Path) 13 | { 14 | @change.OldPath @Html.Raw(" → ") 15 | } 16 | @(removed 17 | ? Html.Raw(change.Path) 18 | : Html.ActionLink(change.Path, "Blob", Html.OverRoute(new { path = (Model.ReferenceName ?? Model.Sha) + "/" + change.Path }))) 19 |
20 |
21 |
22 | } 23 | @foreach (var change in Model.Changes) 24 | { 25 | var removed = change.ChangeKind == LibGit2Sharp.ChangeKind.Deleted || change.ChangeKind == LibGit2Sharp.ChangeKind.Ignored; 26 |
27 |
28 |
29 |
@change.ChangeKind.ToLocateString() +@change.LinesAdded -@change.LinesDeleted
30 |
31 | @if (change.OldPath != change.Path) 32 | { 33 | @change.OldPath @Html.Raw(" → ") 34 | } 35 | @(removed 36 | ? Html.Raw(change.Path) 37 | : Html.ActionLink(change.Path, "Blob", Html.OverRoute(new { path = (Model.ReferenceName ?? Model.Sha) + "/" + change.Path }))) 38 |
39 |
40 | @if (!removed) 41 | { 42 |
43 |
@change.Patch
44 |
45 | } 46 |
47 | } 48 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Blame.cshtml: -------------------------------------------------------------------------------- 1 | @using GitCandy.Web.Extensions 2 | @model GitCandy.Models.BlameModel 3 | 4 | @{ 5 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_BlameTitle, Model.Name, Model.ReferenceName ?? Model.Sha.ToShortSha(), Model.Path)); 6 | } 7 | 8 |

9 | @await Html.PartialAsync("_RepositoryLink", Model) 10 |

11 | 12 | @await Html.PartialAsync("_PathBar", Model.PathBar) 13 | @await Html.PartialAsync("_BranchSelector", Model.BranchSelector) 14 | 15 |
16 |
17 |
18 | @Html.ActionLink(SR.Repository_History, "Commits", new { path = (Model.ReferenceName ?? Model.Sha) + "/" + Model.Path }, new { @class = "btn btn-default" }) 19 | @Html.ActionLink(SR.Repository_NormalView, "Blob", new { path = (Model.ReferenceName ?? Model.Sha) + "/" + Model.Path }, new { @class = "btn btn-default" }) 20 | @Html.ActionLink(SR.Repository_Raw, "Raw", new { path = (Model.ReferenceName ?? Model.Sha) + "/" + Model.Path }, new { @class = "btn btn-default" }) 21 |
22 | @Model.SizeString 23 |
24 |
25 | 26 | 27 | 28 | @foreach (var hunk in Model.Hunks) 29 | { 30 | 31 | 42 | 43 | 44 | } 45 | 46 |
32 |
33 | @Html.ActionLink(hunk.Sha.ToShortSha(), "Commit", new { path = hunk.Sha + "/" + Model.Path }) 34 | @Html.ActionLink("»", "Blame", new { path = hunk.Sha + "/" + Model.Path }) 35 | @hunk.Author 36 |
37 |
38 | @hunk.AuthorDate.LocalDateTime.ToFullString() 39 | @hunk.MessageShort.ShortString(33) 40 |
41 |
@hunk.Code
47 | -------------------------------------------------------------------------------- /GitCandy.Web/Areas/GitCandy/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using NewLife; 2 | using NewLife.Cube; 3 | using NewLife.Cube.ViewModels; 4 | using NewLife.GitCandy.Entity; 5 | using NewLife.Web; 6 | using UserX = NewLife.GitCandy.Entity.User; 7 | 8 | namespace GitCandy.Web.Areas.GitCandy.Controllers; 9 | 10 | [GitCandyArea] 11 | public class UserController : EntityController 12 | { 13 | static UserController() 14 | { 15 | LogOnChange = true; 16 | 17 | ListFields.RemoveCreateField(); 18 | ListFields.RemoveUpdateField(); 19 | ListFields.RemoveRemarkField(); 20 | 21 | { 22 | var df = ListFields.AddDataField("members", null, "IsTeam") as ListField; 23 | df.DisplayName = "成员列表"; 24 | df.Url = "/GitCandy/User?ownerId={ID}"; 25 | df.DataVisible = e => (e as UserX).IsTeam; 26 | } 27 | { 28 | var df = ListFields.AddDataField("userteams", null, "members") as ListField; 29 | df.DisplayName = "关联分组"; 30 | df.Url = "/GitCandy/UserTeam?userId={ID}"; 31 | df.DataVisible = e => !(e as UserX).IsTeam; 32 | } 33 | { 34 | var df = ListFields.AddDataField("repos", "IsAdmin") as ListField; 35 | df.DisplayName = "仓库列表"; 36 | df.Url = "/GitCandy/Repository?ownerId={ID}"; 37 | } 38 | { 39 | var df = ListFields.AddDataField("repos2", "IsAdmin") as ListField; 40 | df.DisplayName = "关联仓库"; 41 | df.Url = "/GitCandy/Repository?userId={ID}"; 42 | df.DataVisible = e => !(e as UserX).IsTeam; 43 | } 44 | } 45 | 46 | protected override IEnumerable Search(Pager p) 47 | { 48 | var id = p["Id"].ToInt(-1); 49 | if (id > 0) 50 | { 51 | var entity = UserX.FindByKey(id); 52 | if (entity != null) return new[] { entity }; 53 | } 54 | 55 | var ownerId = p["ownerId"].ToInt(-1); 56 | var enable = p["enable"]?.ToBoolean(); 57 | var isTeam = p["isTeam"]?.ToBoolean(); 58 | 59 | var start = p["dtStart"].ToDateTime(); 60 | var end = p["dtEnd"].ToDateTime(); 61 | 62 | return UserX.Search(ownerId, enable, isTeam, start, end, p["q"], p); 63 | } 64 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Blob.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.TreeEntryModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_BlobTitle, Model.Name, Model.ReferenceName ?? Model.Sha.ToShortSha(), Model.Path)); 5 | } 6 | 7 |

8 | @await Html.PartialAsync("_RepositoryLink", Model) 9 |

10 | 11 | @await Html.PartialAsync("_PathBar", Model.PathBar) 12 | @await Html.PartialAsync("_BranchSelector", Model.BranchSelector) 13 | 14 |
@Model.Commit.CommitMessageShort
15 |
16 |
17 | @Model.Commit.Author.Name 18 | @SR.Repository_AuthoredAt 19 | @Model.Commit.Author.When.LocalDateTime.ToFullString() 20 | @if (Model.Commit.Author != Model.Commit.Committer) 21 | { 22 | @Model.Commit.Committer.Name 23 | @SR.Repository_CommittedAt 24 | @Model.Commit.Committer.When.LocalDateTime.ToFullString() 25 | } 26 |
27 |
28 |
29 |
30 | @Model.SizeString 31 |
32 | @Html.ActionLink(SR.Repository_History, "Commits", new { path = (Model.ReferenceName ?? Model.Sha) + "/" + Model.Path }, new { @class = "btn btn-default" }) 33 | @Html.ActionLink(SR.Repository_Blame, "Blame", new { path = (Model.ReferenceName ?? Model.Sha) + "/" + Model.Path }, new { @class = "btn btn-default hidden-xs" }) 34 | @Html.ActionLink(SR.Repository_Raw, "Raw", new { path = (Model.ReferenceName ?? Model.Sha) + "/" + Model.Path }, new { @class = "btn btn-default" }) 35 |
36 |
37 |
38 |
39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 |
41 |
@Model.Name
42 |
46 | @await Html.PartialAsync("_BlobPreview") 47 |
51 |
52 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Branches.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.BranchesModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_BranchesTitle, Model)); 5 | } 6 | 7 |

8 | @String.Format(SR.Repository_BranchesTitle, "") @await Html.PartialAsync("_RepositoryLink", Model) 9 |

10 | 11 | @if (Model.Commit == null) 12 | { 13 |
@SR.Repository_HeadNotSet
14 | } 15 | else 16 | { 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | @foreach (var item in Model.AheadBehinds) 31 | { 32 | 33 | 39 | 40 | 41 | 43 | 44 | } 45 | 46 |
21 |
@Model.Commit.ReferenceName
22 | @Model.Commit.Author.Name 23 | @SR.Repository_AuthoredAt 24 | @Model.Commit.Author.When.LocalDateTime.ToFullString() 25 |
@SR.Repository_BaseBranch
34 |
@Html.ActionLink(item.Commit.ReferenceName, "Tree", Html.OverRoute(new { path = item.Commit.ReferenceName }))
35 | @Model.Commit.Author.Name 36 | @SR.Repository_AuthoredAt 37 | @item.Commit.Author.When.LocalDateTime.ToFullString() 38 |

@item.Behind @SR.Repository_Behind

@item.Ahead @SR.Repository_Ahead

@if (Model.CanDelete) 42 | {@SR.Shared_Delete}
47 | } 48 | 49 | 55 | -------------------------------------------------------------------------------- /GitCandy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29503.13 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitCandy", "GitCandy\GitCandy.csproj", "{8C65D667-BA60-4810-927A-F8AFB55D55FE}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitCandy.Web", "GitCandy.Web\GitCandy.Web.csproj", "{D1E514B2-2A79-4101-BB0D-FF2139FFD1EC}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{B011AAFC-9072-4687-B784-FBA91B3F3BBE}" 11 | ProjectSection(SolutionItems) = preProject 12 | LICENSE.md = LICENSE.md 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{F435002A-02CC-4A6B-85AA-994037667505}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {8C65D667-BA60-4810-927A-F8AFB55D55FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {8C65D667-BA60-4810-927A-F8AFB55D55FE}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {8C65D667-BA60-4810-927A-F8AFB55D55FE}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {8C65D667-BA60-4810-927A-F8AFB55D55FE}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {D1E514B2-2A79-4101-BB0D-FF2139FFD1EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {D1E514B2-2A79-4101-BB0D-FF2139FFD1EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {D1E514B2-2A79-4101-BB0D-FF2139FFD1EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {D1E514B2-2A79-4101-BB0D-FF2139FFD1EC}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {F435002A-02CC-4A6B-85AA-994037667505}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {F435002A-02CC-4A6B-85AA-994037667505}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {F435002A-02CC-4A6B-85AA-994037667505}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {F435002A-02CC-4A6B-85AA-994037667505}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {6862CE57-4211-4015-8972-D039F7C98AA6} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /GitCandy.Web/Git/LastCommitAccessor.cs: -------------------------------------------------------------------------------- 1 | using GitCandy.Git.Cache; 2 | using LibGit2Sharp; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.Contracts; 5 | using System; 6 | 7 | namespace GitCandy.Git 8 | { 9 | public sealed class LastCommitAccessor : GitCacheAccessor 10 | { 11 | private readonly Commit commit; 12 | private readonly String path; 13 | 14 | public LastCommitAccessor(String repoId, Repository repo, Commit commit, String path) 15 | : base(repoId, repo) 16 | { 17 | Contract.Requires(commit != null); 18 | Contract.Requires(path != null); 19 | Contract.Requires(commit[path] != null); 20 | 21 | this.commit = commit; 22 | this.path = path; 23 | 24 | var treeEntry = commit[path]; 25 | } 26 | 27 | protected override String GetCacheKey() => GetCacheKey(commit.Sha, path); 28 | 29 | protected override void Init() => _result = commit.Sha; 30 | 31 | protected override void Calculate() 32 | { 33 | using var repo = new Repository(this.repoPath); 34 | var commit = repo.Lookup(this.commit.Sha); 35 | var treeEntry = commit[path]; 36 | if (treeEntry == null) 37 | { 38 | _resultDone = true; 39 | return; 40 | } 41 | 42 | var gitObject = treeEntry.Target; 43 | var hs = new HashSet(); 44 | var queue = new Queue(); 45 | queue.Enqueue(commit); 46 | hs.Add(commit.Sha); 47 | while (queue.Count > 0) 48 | { 49 | commit = queue.Dequeue(); 50 | _result = commit.Sha; 51 | var has = false; 52 | foreach (var parent in commit.Parents) 53 | { 54 | treeEntry = parent[path]; 55 | if (treeEntry == null) 56 | continue; 57 | var eq = treeEntry.Target.Sha == gitObject.Sha; 58 | if (eq && hs.Add(parent.Sha)) 59 | queue.Enqueue(parent); 60 | has = has || eq; 61 | } 62 | if (!has) 63 | break; 64 | } 65 | _resultDone = true; 66 | return; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Contributors.cshtml: -------------------------------------------------------------------------------- 1 | @using GitCandy.Base 2 | @model GitCandy.Models.ContributorsModel 3 | 4 | @{ 5 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_ContributorsTitle, Model)); 6 | } 7 | 8 |

9 | @String.Format(SR.Repository_ContributorsTitle, "") @await Html.PartialAsync("_RepositoryLink", Model) 10 |

11 | 12 |
13 | @foreach (var item in Model.Statistics.Current.OrderedCommits) 14 | { 15 |
16 |
17 |
@item.Author
18 |
@item.CommitsCount @SR.Repository_Commits
19 |
20 |
21 | } 22 |
23 |
24 | 25 | 26 | 27 | @if (Model.Statistics.Default != null) 28 | { 29 | 30 | 31 | 32 | 33 | 34 | 35 | } 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
@SR.Repository_Statistics
@SR.Repository_DefaultBranch@Model.Statistics.Default.Branch
@SR.Repository_DefaultCommits@Model.Statistics.Default.NumberOfCommits
@SR.Repository_DefaultContributors@Model.Statistics.Default.NumberOfContributors
@SR.Repository_DefaultFiles@Model.Statistics.Default.NumberOfFiles
@SR.Repository_DefaultSourceSize@FileHelper.GetSizeString(Model.Statistics.Default.SizeOfSource)
@SR.Repository_CurrentBranch@Model.Statistics.Current.Branch
@SR.Repository_CurrentCommits@Model.Statistics.Current.NumberOfCommits
@SR.Repository_CurrentContributors@Model.Statistics.Current.NumberOfContributors
@SR.Repository_CurrentFiles@Model.Statistics.Current.NumberOfFiles
@SR.Repository_CurrentSourceSize@FileHelper.GetSizeString(Model.Statistics.Current.SizeOfSource)
@SR.Repository_RepositorySize@FileHelper.GetSizeString(Model.Statistics.RepositorySize)
45 |
46 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Detail.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.RepositoryModel 2 | @using GitCandy.Web.App_GlobalResources 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @{ 6 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_DetailTitle, Model.Name)); 7 | var token = ViewBag.Token as IManageUser; 8 | } 9 | 10 |

@String.Format(SR.Repository_DetailTitle, Model.Name)

11 | 12 | @if (Model != null) 13 | { 14 |
15 | 16 |
@Html.DisplayNameFor(s => s.Name)
17 |
@Model.Name
18 | 19 |
@Html.DisplayNameFor(s => s.IsPrivate)
20 |
@Html.DisplayFor(s => s.IsPrivate)
21 | 22 |
@Html.DisplayNameFor(s => s.AllowAnonymousRead)
23 |
@Html.DisplayFor(s => s.AllowAnonymousRead)
24 | 25 |
@Html.DisplayNameFor(s => s.AllowAnonymousWrite)
26 |
@Html.DisplayFor(s => s.AllowAnonymousWrite)
27 | 28 |
@Html.DisplayNameFor(s => s.DefaultBranch)
29 |
@Model.DefaultBranch
30 | 31 |
@Html.DisplayNameFor(s => s.Description)
32 |
@Model.Description
33 | 34 |
@Html.DisplayNameFor(s => s.Collaborators)
35 |
@Html.DisplayFor(s => s.Collaborators)
36 | 37 |
@Html.DisplayNameFor(s => s.Teams)
38 |
@Html.DisplayFor(s => s.Teams)
39 | 40 |
@Html.DisplayNameFor(s => s.Views)
41 |
@Html.DisplayFor(s => s.Views)
42 | 43 |
@Html.DisplayNameFor(s => s.Downloads)
44 |
@Html.DisplayFor(s => s.Downloads)
45 | 46 |
47 | @*@Html.ActionLink(SR.Repository_Tree, "Tree", new { Model.Owner, Model.Name }, new { @class = "btn btn-default pull-left" })*@ 48 | @Html.RouteLink(SR.Repository_Tree, "UserGitWeb", new { Model.Owner, Model.Name }, new { @class = "btn btn-default pull-left" }) 49 |
50 |
51 | @if (token != null && (Model.CurrentUserIsOwner || token.IsAdmin())) 52 | { 53 | @Html.ActionLink(SR.Shared_Edit, "Edit", new { Model.Id }, new { @class = "btn btn-primary" }) 54 | @: 55 | @Html.ActionLink(SR.Repository_Relationship, "Coop", new { Model.Id }, new { @class = "btn btn-info" }) 56 | } 57 |
58 |
59 | } 60 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/TeamModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using GitCandy.Base; 3 | using GitCandy.Web.App_GlobalResources; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class TeamModel 8 | { 9 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 10 | [StringLength(20, MinimumLength = 2, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 11 | [RegularExpression(RegularExpression.Teamname, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Name")] 12 | [Display(ResourceType = typeof(SR), Name = "Team_Name")] 13 | public String Name { get; set; } 14 | 15 | [StringLength(20, MinimumLength = 2, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLength")] 16 | [Display(ResourceType = typeof(SR), Name = "Account_Nickname")] 17 | [DisplayFormat(ConvertEmptyStringToNull = false)] 18 | public String Nickname { get; set; } 19 | 20 | [StringLength(500, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLength")] 21 | [Display(ResourceType = typeof(SR), Name = "Team_Description")] 22 | [DisplayFormat(ConvertEmptyStringToNull = false)] 23 | public String Description { get; set; } 24 | 25 | [Display(ResourceType = typeof(SR), Name = "Team_Members")] 26 | [UIHint("Maps")] 27 | //[AdditionalMetadata("Controller", "Account")] 28 | public IDictionary Members { get; set; } 29 | 30 | public UserRole[] MembersRole { get; set; } 31 | 32 | [Display(ResourceType = typeof(SR), Name = "Team_Repositories")] 33 | [UIHint("Members")] 34 | //[AdditionalMetadata("Controller", "Repository")] 35 | public String[] Repositories { get; set; } 36 | 37 | public RepositoryRole[] RepositoriesRole { get; set; } 38 | 39 | public class UserRole 40 | { 41 | public String Name { get; set; } 42 | public String NickName { get; set; } 43 | public Boolean IsAdministrator { get; set; } 44 | } 45 | 46 | public class RepositoryRole 47 | { 48 | public String Name { get; set; } 49 | public Boolean AllowRead { get; set; } 50 | public Boolean AllowWrite { get; set; } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /GitCandy/Security/Token.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Web; 3 | 4 | namespace GitCandy.Security 5 | { 6 | public class Token 7 | { 8 | private const String ContentKey = "GitCandyToken"; 9 | private const Double AuthPeriod = 21600; // 15 days 10 | private const Double RenewalPeriod = 1440; // 1 day 11 | 12 | private Token() { } 13 | 14 | public Token(String authCode, Int32 userID, String username, String nickname, Boolean isSystemAdministrator, DateTime? expires = null) 15 | { 16 | AuthCode = new Guid(authCode); 17 | UserID = userID; 18 | Username = username; 19 | Nickname = nickname; 20 | IsAdmin = isSystemAdministrator; 21 | 22 | var now = DateTime.Now; 23 | IssueDate = now; 24 | Expires = expires ?? now.AddMinutes(AuthPeriod); 25 | } 26 | 27 | //public static Token Current 28 | //{ 29 | // get 30 | // { 31 | // var context = HttpContext.Current; 32 | // if (context == null) return null; 33 | 34 | // return context.Items[ContentKey] as Token; 35 | // } 36 | // set 37 | // { 38 | // var context = HttpContext.Current; 39 | // if (context == null) return; 40 | 41 | // context.Items[ContentKey] = value; 42 | // } 43 | //} 44 | 45 | public Guid AuthCode { get; private set; } 46 | public Int32 UserID { get; private set; } 47 | public String Username { get; private set; } 48 | public String Nickname { get; private set; } 49 | public Boolean IsAdmin { get; private set; } 50 | 51 | public DateTime Expires { get; private set; } 52 | public DateTime IssueDate { get; private set; } 53 | public String LastIp { get; set; } 54 | 55 | public Boolean Expired => Expires > DateTime.Now.AddMinutes(AuthPeriod); 56 | 57 | public Boolean RenewIfNeed() 58 | { 59 | var now = DateTime.Now; 60 | if (Expires > now && (Expires - now).TotalMinutes < AuthPeriod - RenewalPeriod) 61 | { 62 | Expires = now.AddMinutes(AuthPeriod); 63 | return true; 64 | } 65 | return false; 66 | } 67 | 68 | public static DateTime AuthorizationExpires => DateTime.Now.AddMinutes(AuthPeriod); 69 | } 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitCandy 2 | GitCandy© 是一个基于 ASP.NET MVC 的 [Git](http://git-scm.com/documentation) 版本控制服务端,支持公有和私有代码库,可不受限制的创建代码代码库,随时随地的与团队进行协作。 3 | 4 | GitCandy© 由团队成员[Aimeast](https://github.com/Aimeast/GitCandy)创建,本分支引入[魔方](http://git.newlifex.com/NewLife/X/Tree/master/NewLife.Cube)并进行功能调整,主要改进为免部署,以及支持团队个人下属源码库两级管理。 5 | 6 | 演示:[http://git.newlifex.com/](http://git.newlifex.com/) 7 | 8 | 源码: http://git.NewLifeX.com/NewLife/GitCandy 9 | 海外: https://github.com/NewLifeX/GitCandy 10 | 11 | GitCandy官方群:200319579 新生命群:1600800 12 | 13 | --- 14 | ### 系统要求 15 | * [IIS 7.0](http://www.iis.net/learn) 16 | * [.NET Framework 4.5](http://www.microsoft.com/en-us/download/details.aspx?id=30653) 17 | * [ASP.NET MVC 5](http://www.asp.net/mvc/tutorials/mvc-5) 18 | * [Git](http://git-for-windows.github.io/) 19 | * [Sqlite](http://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki) 或 [Sql Server](http://www.microsoft.com/en-us/sqlserver/get-sql-server/try-it.aspx) 20 | 21 | --- 22 | ### 安装 23 | * 下载最新[发布](https://github.com/NewLifeX/GitCandy/releases)的版本或自己编译最新的[master](http://git.newlifex.com/NewLife/GitCandy)分支源码 24 | * 在IIS创建一个站点,并把二进制文件和资源文件复制到站点目录 25 | * 如果用了 Visual Studio 的发布功能,还要复制`GitCandy\bin\[NativeBinaries & x86 & x64]`文件夹到站点目录 26 | * 打开新建的站点,默认登录用户名是`admin`,密码是`gitcandy` 27 | * 转到`设置`页面,分别设置`代码库`,`缓存`和`git-core`的路径 28 | * 推荐在`Web.config`设置`` 29 | 30 | ##### *注* 31 | * `代码库`和`缓存`路径示例:`x:\Repos`,`x:\Cache` 32 | * `git-core`路径示例:`x:\PortableGit\libexec\git-core`,`x:\PortableGit\mingw64\libexec\git-core` 33 | 34 | --- 35 | ### 鸣谢 (按字母序) 36 | * [ASP.NET MVC](http://aspnetwebstack.codeplex.com/) @ [Apache License 2.0](http://aspnetwebstack.codeplex.com/license) 37 | * [Bootstrap](http://github.com/twbs/bootstrap) @ [MIT License](http://github.com/twbs/bootstrap/blob/master/LICENSE) 38 | * [Bootstrap-switch](http://github.com/nostalgiaz/bootstrap-switch) @ [Apache License 2.0](http://github.com/nostalgiaz/bootstrap-switch/blob/master/LICENSE) 39 | * [Highlight.js](http://github.com/isagalaev/highlight.js) @ [New BSD License](http://github.com/isagalaev/highlight.js/blob/master/LICENSE) 40 | * [jQuery](http://github.com/jquery/jquery) @ [MIT License](http://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt) 41 | * [LibGit2Sharp](http://github.com/libgit2/libgit2sharp) @ [MIT License](http://github.com/libgit2/libgit2sharp/blob/master/LICENSE.md) 42 | * [marked](http://github.com/chjj/marked) @ [MIT License](http://github.com/chjj/marked/blob/master/LICENSE) 43 | 44 | --- 45 | ### 协议 46 | MIT 协议 47 | -------------------------------------------------------------------------------- /GitCandy.Web/Git/HistoryDivergenceAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Linq; 4 | using GitCandy.Git.Cache; 5 | using LibGit2Sharp; 6 | 7 | namespace GitCandy.Git 8 | { 9 | public class HistoryDivergenceAccessor : GitCacheAccessor 10 | { 11 | private readonly String key; 12 | 13 | public HistoryDivergenceAccessor(String repoId, Repository repo, String key) 14 | : base(repoId, repo) 15 | { 16 | Contract.Requires(key != null); 17 | 18 | this.key = key; 19 | } 20 | 21 | protected override String GetCacheKey() => GetCacheKey(key); 22 | 23 | protected override void Init() 24 | { 25 | var head = repo.Head; 26 | if (head.Tip == null) 27 | return; 28 | 29 | _result = repo.Branches 30 | .Where(s => s != head && s.FriendlyName != "HEAD") 31 | .OrderByDescending(s => s.Tip.Author.When) 32 | .Select(branch => 33 | { 34 | var commit = branch.Tip; 35 | return new RevisionSummaryCacheItem 36 | { 37 | Ahead = 0, 38 | Behind = 0, 39 | Name = branch.FriendlyName, 40 | CommitSha = commit.Sha, 41 | AuthorName = commit.Author.Name, 42 | AuthorEmail = commit.Author.Email, 43 | AuthorWhen = commit.Author.When, 44 | CommitterName = commit.Committer.Name, 45 | CommitterEmail = commit.Committer.Email, 46 | CommitterWhen = commit.Committer.When, 47 | }; 48 | }) 49 | .ToArray(); 50 | } 51 | 52 | protected override void Calculate() 53 | { 54 | using (var repo = new Repository(this.repoPath)) 55 | { 56 | var head = repo.Head; 57 | foreach (var item in _result) 58 | { 59 | var commit = repo.Branches[item.Name].Tip; 60 | var divergence = repo.ObjectDatabase.CalculateHistoryDivergence(commit, head.Tip); 61 | item.Ahead = divergence.AheadBy ?? 0; 62 | item.Behind = divergence.BehindBy ?? 0; 63 | } 64 | } 65 | _resultDone = true; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.UserModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Account_CreateTitle); 5 | } 6 | 7 |

@SR.Account_CreateTitle

8 | 9 | 10 | @using (Html.BeginForm("Create", "Account", FormMethod.Post)) 11 | { 12 |
13 | 14 |
@Html.DisplayNameFor(s => s.Name)
15 |
@Html.TextBoxFor(s => s.Name, new { @class = "form-control" })
16 |
17 | 18 | @Html.ValidationMessageFor(s => s.Name) 19 | 20 |
21 | 22 |
@Html.DisplayNameFor(s => s.Nickname)
23 |
@Html.TextBoxFor(s => s.Nickname, new { @class = "form-control" })
24 |
25 | 26 | @Html.ValidationMessageFor(s => s.Nickname) 27 | 28 |
29 | 30 |
@Html.DisplayNameFor(s => s.Password)
31 |
@Html.PasswordFor(s => s.Password, new { @class = "form-control" })
32 |
33 | 34 | @Html.ValidationMessageFor(s => s.Password) 35 | 36 |
37 | 38 |
@Html.DisplayNameFor(s => s.ConformPassword)
39 |
@Html.PasswordFor(s => s.ConformPassword, new { @class = "form-control" })
40 |
41 | 42 | @Html.ValidationMessageFor(s => s.ConformPassword) 43 | 44 |
45 | 46 |
@Html.DisplayNameFor(s => s.Email)
47 |
@Html.TextBoxFor(s => s.Email, new { @class = "form-control" })
48 |
49 | 50 | @Html.ValidationMessageFor(s => s.Email) 51 | 52 |
53 | 54 |
@Html.DisplayNameFor(s => s.Description)
55 |
@Html.TextAreaFor(s => s.Description, 4, 0, new { @class = "form-control" })
56 |
57 | 58 | @Html.ValidationMessageFor(s => s.Description) 59 | 60 |
61 | 62 |
63 |
@Html.ValidationSummary(true, SR.Account_CreationUnsuccessfull, new { @class = "alert alert-dismissable alert-danger" })
64 | 65 |
66 |
67 |   68 |   69 |
70 |
71 | } 72 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/SettingModel.cs: -------------------------------------------------------------------------------- 1 | using GitCandy.Web.App_GlobalResources; 2 | using System.ComponentModel.DataAnnotations; 3 | using System; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class SettingModel 8 | { 9 | [Display(ResourceType = typeof(SR), Name = "Setting_IsPublicServer")] 10 | public Boolean IsPublicServer { get; set; } 11 | 12 | [Display(ResourceType = typeof(SR), Name = "Setting_ForceSsl")] 13 | public Boolean ForceSsl { get; set; } 14 | 15 | [Range(1, 65534, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_NumberRange")] 16 | [Display(ResourceType = typeof(SR), Name = "Setting_SslPort")] 17 | public Int32 SslPort { get; set; } 18 | 19 | [Range(1, 65534, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_NumberRange")] 20 | [Display(ResourceType = typeof(SR), Name = "Setting_SshPort")] 21 | public Int32 SshPort { get; set; } 22 | 23 | [Display(ResourceType = typeof(SR), Name = "Setting_EnableSsh")] 24 | public Boolean EnableSsh { get; set; } 25 | 26 | [Display(ResourceType = typeof(SR), Name = "Setting_LocalSkipCustomError")] 27 | public Boolean LocalSkipCustomError { get; set; } 28 | 29 | [Display(ResourceType = typeof(SR), Name = "Setting_AllowRegisterUser")] 30 | public Boolean AllowRegisterUser { get; set; } 31 | 32 | [Display(ResourceType = typeof(SR), Name = "Setting_AllowRepositoryCreation")] 33 | public Boolean AllowRepositoryCreation { get; set; } 34 | 35 | [Display(ResourceType = typeof(SR), Name = "Setting_RepositoryPath")] 36 | public String RepositoryPath { get; set; } 37 | 38 | [Display(ResourceType = typeof(SR), Name = "Setting_CachePath")] 39 | public String CachePath { get; set; } 40 | 41 | [Display(ResourceType = typeof(SR), Name = "Setting_GitCorePath")] 42 | public String GitCorePath { get; set; } 43 | 44 | [Range(5, 50, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_NumberRange")] 45 | [Display(ResourceType = typeof(SR), Name = "Setting_NumberOfCommitsPerPage")] 46 | public Int32 NumberOfCommitsPerPage { get; set; } 47 | 48 | [Range(5, 50, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_NumberRange")] 49 | [Display(ResourceType = typeof(SR), Name = "Setting_NumberOfItemsPerList")] 50 | public Int32 NumberOfItemsPerList { get; set; } 51 | 52 | [Range(10, 100, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_NumberRange")] 53 | [Display(ResourceType = typeof(SR), Name = "Setting_NumberOfRepositoryContributors")] 54 | public Int32 NumberOfRepositoryContributors { get; set; } 55 | } 56 | } -------------------------------------------------------------------------------- /GitCandy.Web/Git/BlameAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using GitCandy.Base; 7 | using GitCandy.Git.Cache; 8 | using GitCandy.Models; 9 | using GitCandy.Web.Extensions; 10 | using LibGit2Sharp; 11 | 12 | namespace GitCandy.Git 13 | { 14 | public class BlameAccessor : GitCacheAccessor 15 | { 16 | private readonly Commit commit; 17 | private readonly String path, code; 18 | 19 | public BlameAccessor(String repoId, Repository repo, Commit commit, String path, params Encoding[] encodings) 20 | : base(repoId, repo) 21 | { 22 | Contract.Requires(commit != null); 23 | Contract.Requires(path != null); 24 | Contract.Requires(encodings != null); 25 | Contract.Requires(commit[path] != null); 26 | Contract.Requires(commit[path].TargetType == TreeEntryTargetType.Blob); 27 | 28 | var treeEntry = commit[path]; 29 | 30 | this.commit = commit; 31 | this.path = path; 32 | 33 | var blob = (Blob)treeEntry.Target; 34 | var bytes = blob.GetContentStream().ToBytes(); 35 | var encoding = FileHelper.DetectEncoding(bytes, encodings); 36 | this.code = FileHelper.ReadToEnd(bytes, encoding); 37 | } 38 | 39 | protected override String GetCacheKey() => GetCacheKey(commit.Sha, path); 40 | 41 | protected override void Init() 42 | { 43 | _result = 44 | [ 45 | new() { 46 | Code = code, 47 | MessageShort = commit.MessageShort.RepetitionIfEmpty(GitService.UnknowString), 48 | Sha = commit.Sha, 49 | Author = commit.Author.Name, 50 | AuthorEmail = commit.Author.Email, 51 | AuthorDate = commit.Author.When, 52 | } 53 | ]; 54 | } 55 | 56 | protected override void Calculate() 57 | { 58 | using (var repo = new Repository(this.repoPath)) 59 | { 60 | var reader = new StringReader(code); 61 | var blame = repo.Blame(path, new BlameOptions { StartingAt = commit }); 62 | _result = blame.Select(s => new BlameHunkModel 63 | { 64 | Code = reader.ReadLines(s.LineCount), 65 | MessageShort = s.FinalCommit.MessageShort.RepetitionIfEmpty(GitService.UnknowString), 66 | Sha = s.FinalCommit.Sha, 67 | Author = s.FinalCommit.Author.Name, 68 | AuthorEmail = s.FinalCommit.Author.Email, 69 | AuthorDate = s.FinalCommit.Author.When, 70 | }) 71 | .ToArray(); 72 | } 73 | _resultDone = true; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /GitCandy.Web/Controllers/CandyControllerBase.cs: -------------------------------------------------------------------------------- 1 | using GitCandy.Data; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using NewLife.Cube; 6 | using NewLife.Model; 7 | using XCode.Membership; 8 | using UserX = NewLife.GitCandy.Entity.User; 9 | 10 | namespace GitCandy.Web.Controllers; 11 | 12 | [AllowAnonymous] 13 | public abstract class CandyControllerBase : Controller 14 | { 15 | //private const String AuthKey = "_gc_auth"; 16 | 17 | //private Token _token; 18 | 19 | /// 临时会话扩展信息。仅限本地内存,不支持分布式共享 20 | public IDictionary Session { get; private set; } 21 | 22 | /// 用户主机 23 | public String UserHost { get; private set; } 24 | 25 | public MembershipService MembershipService { get; set; } = new MembershipService(); 26 | 27 | /// 当前用户 28 | public IManageUser Token { get; private set; } 29 | 30 | public override void OnActionExecuting(ActionExecutingContext filterContext) 31 | { 32 | // 进程内模拟的Session,活跃有效期20分钟 33 | var ctx = filterContext.HttpContext; 34 | Session = ctx.Items["Session"] as IDictionary; 35 | 36 | UserHost = HttpContext.GetUserHost(); 37 | 38 | Token = Session["GitToken"] as IManageUser; 39 | if (Token == null) 40 | { 41 | // 如果未登录,则自动跳转登录 42 | var provider = ManageProvider.Provider; 43 | var user = provider.Current ?? provider.TryLogin(HttpContext); 44 | //if (user == null) 45 | //{ 46 | // var url = "/Admin/User/Login"; 47 | // var returnUrl = Request.GetRawUrl(); 48 | // if (returnUrl != null) url += "?r=" + HttpUtility.UrlEncode(returnUrl.PathAndQuery); 49 | 50 | // filterContext.Result = new RedirectResult(url); 51 | 52 | // return; 53 | //} 54 | 55 | // 自动创建本地用户 56 | if (user != null) 57 | { 58 | Token = UserX.GetOrAdd(user); 59 | Session["GitToken"] = Token; 60 | } 61 | } 62 | 63 | ViewBag.Token = Token; 64 | 65 | // 语言文化 66 | var culture = Thread.CurrentThread.CurrentUICulture; 67 | var displayName = culture.Name.StartsWith("en") 68 | ? culture.NativeName 69 | : culture.EnglishName + " - " + culture.NativeName; 70 | 71 | ViewBag.Language = displayName; 72 | ViewBag.Lang = culture.Name; 73 | ViewBag.Identity = 0; 74 | 75 | base.OnActionExecuting(filterContext); 76 | } 77 | 78 | protected virtual ActionResult RedirectToStartPage(String returnUrl = null) 79 | { 80 | if (String.IsNullOrEmpty(returnUrl) || !Url.IsLocalUrl(returnUrl)) return RedirectToAction("Index", "Repository"); 81 | 82 | return Redirect(returnUrl); 83 | } 84 | 85 | //public virtual new ActionResult Forbid() => StatusCode(403); 86 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Edit.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.RepositoryModel 2 | @using GitCandy.Web.App_GlobalResources 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @{ 6 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Repository_EditTitle, Model.Name)); 7 | var token = ViewBag.Token as IManageUser; 8 | } 9 | 10 |

@String.Format(SR.Repository_EditTitle, Model.Name)

11 | 12 | 13 | @using (Html.BeginForm("Edit", "Repository", FormMethod.Post)) 14 | { 15 |
16 |
@Html.DisplayNameFor(s => s.Name)
17 |
18 | @Html.HiddenFor(s => s.Name) 19 | @Model.Name 20 |
21 | 22 |
@Html.DisplayNameFor(s => s.IsPrivate)
23 |
24 |
25 | @Html.CheckBoxFor(s => s.IsPrivate, new { data_size = "small" }) 26 |
27 |
28 | 29 |
@Html.DisplayNameFor(s => s.AllowAnonymousRead)
30 |
31 |
32 | @Html.CheckBoxFor(s => s.AllowAnonymousRead, new { data_size = "small" }) 33 |
34 |
35 | 36 |
@Html.DisplayNameFor(s => s.AllowAnonymousWrite)
37 |
38 |
39 | @Html.CheckBoxFor(s => s.AllowAnonymousWrite, new { data_size = "small" }) 40 |
41 |
42 | 43 |
@Html.DisplayNameFor(s => s.DefaultBranch)
44 |
@Html.DropDownListFor(s => s.DefaultBranch, Model.LocalBranches.ToSelectListItem(Model.DefaultBranch), new { @class = "form-control" })
45 | 46 |
@Html.DisplayNameFor(s => s.Description)
47 |
@Html.TextAreaFor(s => s.Description, 4, 0, new { @class = "form-control" })
48 |
49 | 50 | @Html.ValidationMessageFor(s => s.Description) 51 | 52 |
53 | 54 |
55 |
@Html.ValidationSummary(true, SR.Repository_UpdateUnsuccessfull, new { @class = "alert alert-dismissable alert-danger" })
56 | 57 |
58 | @Html.ActionLink(SR.Shared_Back, "Detail", new { Model.Id }, new { @class = "btn btn-default pull-left" }) 59 |
60 |
61 |   62 |   63 | @if (token != null && (Model.CurrentUserIsOwner || token.IsAdmin())) 64 | { 65 | @Html.ActionLink(SR.Shared_Delete, "Delete", new { Model.Id }, new { @class = "btn btn-danger" }) 66 | } 67 |
68 |
69 | } 70 | -------------------------------------------------------------------------------- /GitCandy.Web/Models/RepositoryModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using GitCandy.Base; 3 | using GitCandy.Web.App_GlobalResources; 4 | 5 | namespace GitCandy.Models; 6 | 7 | public class RepositoryModel 8 | { 9 | public Int32 Id { get; set; } 10 | 11 | [Display(Name = "拥有者")] 12 | //[Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 13 | public String Owner { get; set; } 14 | 15 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 16 | [StringLength(50, MinimumLength = 2, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 17 | [RegularExpression(RegularExpression.Repositoryname, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Name")] 18 | [Display(ResourceType = typeof(SR), Name = "Repository_Name")] 19 | public String Name { get; set; } 20 | 21 | [StringLength(500, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLength")] 22 | [Display(ResourceType = typeof(SR), Name = "Repository_Description")] 23 | [DisplayFormat(ConvertEmptyStringToNull = false)] 24 | public String Description { get; set; } 25 | 26 | [Display(ResourceType = typeof(SR), Name = "Repository_HowInit")] 27 | public String HowInit { get; set; } 28 | 29 | [Display(ResourceType = typeof(SR), Name = "Repository_RemoteUrlTitle")] 30 | public String RemoteUrl { get; set; } 31 | 32 | [Display(ResourceType = typeof(SR), Name = "Repository_IsPrivate")] 33 | [UIHint("YesNo")] 34 | public Boolean IsPrivate { get; set; } 35 | 36 | [Display(ResourceType = typeof(SR), Name = "Repository_AllowAnonymousRead")] 37 | [UIHint("YesNo")] 38 | public Boolean AllowAnonymousRead { get; set; } 39 | 40 | [Display(ResourceType = typeof(SR), Name = "Repository_AllowAnonymousWrite")] 41 | [UIHint("YesNo")] 42 | public Boolean AllowAnonymousWrite { get; set; } 43 | 44 | [Display(ResourceType = typeof(SR), Name = "Repository_Collaborators")] 45 | [UIHint("Maps")] 46 | //[AdditionalMetadata("Controller", "Account")] 47 | public IDictionary Collaborators { get; set; } 48 | 49 | [Display(ResourceType = typeof(SR), Name = "Repository_Teams")] 50 | [UIHint("Maps")] 51 | //[AdditionalMetadata("Controller", "Team")] 52 | public IDictionary Teams { get; set; } 53 | 54 | [Display(ResourceType = typeof(SR), Name = "Repository_DefaultBranch")] 55 | public String DefaultBranch { get; set; } 56 | 57 | public String[] LocalBranches { get; set; } 58 | 59 | public Boolean CurrentUserIsOwner { get; set; } 60 | 61 | public Int32 Commits { get; set; } 62 | public Int32 Branches { get; set; } 63 | public Int32 Contributors { get; set; } 64 | public DateTime LastCommit { get; set; } 65 | [Display(Name = "浏览数")] 66 | public Int32 Views { get; set; } 67 | public DateTime LastView { get; set; } 68 | [Display(Name = "下载数")] 69 | public Int32 Downloads { get; set; } 70 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Index.cshtml: -------------------------------------------------------------------------------- 1 | @using GitCandy.Models 2 | @model GitCandy.Models.RepositoryListModel 3 | 4 | @{ 5 | //ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Repository_ListTitle); 6 | ViewBag.Title = ""; 7 | if (Model.Collaborations.Length == 0) 8 | { 9 | Model.Collaborations = Model.Repositories; 10 | Model.Repositories = new RepositoryModel[0]; 11 | } 12 | } 13 |
14 |
15 | 16 | @foreach (var repo in Model.Collaborations) 17 | { 18 | var name = repo.Name.Contains(repo.Owner) ? repo.Name : String.Format("{0}/{1}", repo.Owner, repo.Name); 19 | 20 | 32 | 33 | } 34 |
21 | 22 |
@repo.Description
23 |
24 |
25 | @repo.Views.ToString("n0") 26 | @repo.Commits.ToString("n0") 27 | @repo.Contributors.ToString("n0") 28 |
29 |
@repo.LastCommit.ToFullString()
30 |
31 |
35 |
36 | 37 |
38 | 39 | @SR.Repository_PopularRepositories 40 | @foreach (var repo in Model.Repositories) 41 | { 42 | var name = repo.Name.Contains(repo.Owner) ? repo.Name : String.Format("{0}/{1}", repo.Owner, repo.Name); 43 | 44 | 56 | 57 | } 58 |
45 | 46 |
@repo.Description
47 |
48 |
49 | @repo.Views.ToString("n0") 50 | @repo.Commits.ToString("n0") 51 | @repo.Contributors.ToString("n0") 52 |
53 |
@repo.LastCommit.ToFullString()
54 |
55 |
59 |
60 |
61 | @await Html.PartialAsync("_Pager") -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/_BranchSelector.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.BranchSelectorModel 2 | @{ 3 | var values = ViewContext.RouteData.Values; 4 | var action = values["action"] as String; 5 | var noLink = Model.Path == null; 6 | //var identity = Html.GetRootViewBag().Identity++; 7 | var identity = ViewBag.Identity++; 8 | } 9 | 61 | -------------------------------------------------------------------------------- /GitCandy.Web/Services/AccountService.cs: -------------------------------------------------------------------------------- 1 | using NewLife; 2 | using NewLife.Cube.Entity; 3 | using NewLife.Log; 4 | using NewLife.Web; 5 | using XCode.Membership; 6 | using UserX = NewLife.GitCandy.Entity.User; 7 | 8 | namespace GitCandy.Web.Services; 9 | 10 | public class AccountService 11 | { 12 | private readonly ITracer _tracer; 13 | 14 | public AccountService(ITracer tracer) => _tracer = tracer; 15 | 16 | public UserX Login(String username, String password, String ip) 17 | { 18 | using var span = _tracer?.NewSpan("AccountLogin", new { username, ip }); 19 | 20 | var user = UserX.Check(username); 21 | if (user != null && user.Enable) 22 | { 23 | var md5 = password.MD5(); 24 | 25 | // 基础用户表找到该用户 26 | var provider = ManageProvider.Provider; 27 | var u = provider.FindByID(user.LinkID); 28 | if (u != null && u.Enable) 29 | { 30 | { 31 | using var span2 = _tracer?.NewSpan("AccountLogin-Token", new { u.Name, ip }); 32 | try 33 | { 34 | // 以用户令牌登录,替代密码,更安全 35 | //var tokens = UserToken.FindAllByUserID(user.LinkID); 36 | var pager = new Pager { PageSize = 100 }; 37 | var tokens = UserToken.Search(null, user.LinkID, true, DateTime.MinValue, DateTime.MinValue, pager); 38 | foreach (var token in tokens) 39 | { 40 | if (token.Enable && (token.Expire.Year < 1000 || token.Expire > DateTime.Now) && token.Token.EqualIgnoreCase(password, md5)) 41 | { 42 | user.Login(ip); 43 | 44 | return user; 45 | } 46 | } 47 | } 48 | catch (Exception ex) 49 | { 50 | span?.SetError(ex, null); 51 | throw; 52 | } 53 | } 54 | 55 | { 56 | using var span2 = _tracer?.NewSpan("AccountLogin-Password", new { u.Name, ip }); 57 | try 58 | { 59 | // 基础用户表中验证用户密码 60 | u = ManageProvider.Provider.Login(u.Name, password, false); 61 | if (u != null) 62 | { 63 | user.Login(ip); 64 | 65 | return user; 66 | } 67 | } 68 | catch (Exception ex) 69 | { 70 | span?.SetError(ex, null); 71 | throw; 72 | } 73 | } 74 | } 75 | // 继续使用原来的密码验证 76 | else 77 | { 78 | if (user.Password == md5) 79 | { 80 | using var span2 = _tracer?.NewSpan("AccountLogin-Old", user.Name); 81 | user.Login(ip); 82 | 83 | return user; 84 | } 85 | } 86 | } 87 | 88 | return null; 89 | } 90 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Account/Edit.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.UserModel 2 | @using GitCandy.Web.App_GlobalResources 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @{ 6 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, String.Format(SR.Account_EditTitle, Model.Name)); 7 | var token = ViewBag.Token as IManageUser; 8 | } 9 | 10 |

@String.Format(SR.Account_EditTitle, Model.Name)

11 | 12 | 13 | @using (Html.BeginForm("Edit", "Account", FormMethod.Post)) 14 | { 15 |
16 |
@Html.DisplayNameFor(s => s.Name)
17 |
18 | @Html.HiddenFor(s => s.Name) 19 | @Model.Name 20 |
21 | 22 |
@Html.DisplayNameFor(s => s.Password)
23 |
@Html.PasswordFor(s => s.Password, new { @class = "form-control" })
24 |
25 | 26 | @Html.ValidationMessageFor(s => s.Password) 27 | 28 | @if (token.IsAdmin()) 29 | { 30 | 31 | 使用当前管理员密码确认是否合法操作 32 | 33 | } 34 |
35 | 36 |
@Html.DisplayNameFor(s => s.Nickname)
37 |
@Html.TextBoxFor(s => s.Nickname, new { @class = "form-control" })
38 |
39 | 40 | @Html.ValidationMessageFor(s => s.Nickname) 41 | 42 |
43 | 44 |
@Html.DisplayNameFor(s => s.Email)
45 |
@Html.TextBoxFor(s => s.Email, new { @class = "form-control" })
46 |
47 | 48 | @Html.ValidationMessageFor(s => s.Email) 49 | 50 |
51 | 52 |
@Html.DisplayNameFor(s => s.Description)
53 |
@Html.TextAreaFor(s => s.Description, 4, 0, new { @class = "form-control" })
54 |
55 | 56 | @Html.ValidationMessageFor(s => s.Description) 57 | 58 |
59 | 60 | @*@if (token.IsAdmin()) 61 | { 62 |
@Html.DisplayNameFor(s => s.IsAdmin)
63 |
64 |
65 | @Html.CheckBoxFor(s => s.IsAdmin, new { data_size = "small" }) 66 |
67 |
68 | }*@ 69 | 70 |
71 |
@Html.ValidationSummary(true, SR.Account_UpdateUnsuccessfull, new { @class = "alert alert-dismissable alert-danger" })
72 | 73 |
74 | @Html.ActionLink(SR.Shared_Back, "Detail", new { Model.Name }, new { @class = "btn btn-default pull-left" }) 75 |
76 |
77 |   78 |   79 | @if (token != null && token.IsAdmin()) 80 | { 81 | @Html.ActionLink(SR.Shared_Delete, "Delete", new { Model.Name }, new { @class = "btn btn-danger" }) 82 | } 83 |
84 |
85 | } 86 | -------------------------------------------------------------------------------- /GitCandy.Web/Extensions/HtmlHelperExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using GitCandy.Models; 3 | using Microsoft.AspNetCore.Html; 4 | using Microsoft.AspNetCore.Mvc.Rendering; 5 | using NewLife.GitCandy.Entity; 6 | 7 | namespace GitCandy.Web.Extensions; 8 | 9 | public static class HtmlHelperExtension 10 | { 11 | public static RouteValueDictionary OverRoute(this IHtmlHelper helper, Object routeValues = null, Boolean withQuery = false) 12 | { 13 | var old = helper.ViewContext.RouteData.Values; 14 | if (routeValues == null) return old; 15 | 16 | var over = new Dictionary(old, StringComparer.OrdinalIgnoreCase); 17 | if (withQuery) 18 | { 19 | var qs = helper.ViewContext.HttpContext.Request.Query; 20 | foreach (var item in qs) 21 | { 22 | over[item.Key] = item.Value; 23 | } 24 | } 25 | var values = new RouteValueDictionary(routeValues); 26 | foreach (var pair in values) 27 | { 28 | over[pair.Key] = pair.Value; 29 | } 30 | 31 | return new RouteValueDictionary(over); 32 | } 33 | 34 | //public static HtmlString ActionLink(this IHtmlHelper htmlHelper, String linkText, String actionName, RouteValueDictionary routeValues, Object htmlAttributes) 35 | //{ 36 | // return htmlHelper.ActionLink(linkText, actionName, routeValues, htmlAttributes.CastToDictionary()); 37 | //} 38 | 39 | //public static HtmlString ActionLink(this IHtmlHelper htmlHelper, String linkText, String actionName, String controllerName, RouteValueDictionary routeValues, Object htmlAttributes) 40 | //{ 41 | // return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes.CastToDictionary()); 42 | //} 43 | 44 | public static IHtmlContent CultureActionLink(this IHtmlHelper htmlHelper, String langName) 45 | { 46 | var culture = CultureInfo.CreateSpecificCulture(langName); 47 | var displayName = culture.Name.StartsWith("en") 48 | ? culture.NativeName 49 | : culture.EnglishName + " - " + culture.NativeName; 50 | 51 | return htmlHelper.ActionLink(displayName, "Language", "Home", new { Lang = culture.Name }, null); 52 | } 53 | 54 | //public static dynamic GetRootViewBag(this IHtmlHelper html) 55 | //{ 56 | // var controller = html.ViewContext.Controller; 57 | // while (controller.ControllerContext.IsChildAction) 58 | // { 59 | // controller = controller.ControllerContext.ParentActionViewContext.Controller; 60 | // } 61 | 62 | // return controller.ViewBag; 63 | //} 64 | 65 | public static HtmlString Link(this IHtmlHelper html, RepositoryModelBase repo) 66 | { 67 | if (repo == null) return null; 68 | 69 | var user = User.FindByName(repo.Owner); 70 | if (user == null) return null; 71 | 72 | var link1 = html.ActionLink(repo.Owner, "Detail", user.IsTeam ? "Team" : "Account", new { name = repo.Owner }, null); 73 | //var link2 = html.ActionLink(repo.Name, "Tree", new { owner = repo.Owner, name = repo.Name, path = "" }); 74 | var link2 = html.RouteLink(repo.Name, "UserGitWeb", new { owner = repo.Owner, name = repo.Name }); 75 | 76 | return new HtmlString(link1.ToString() + "/" + link2.ToString()); 77 | } 78 | } -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/Content/Site.css: -------------------------------------------------------------------------------- 1 | .list-toolbar .btn-sm-force, .btn-toolbar .btn-sm-force { 2 | height: 33px; 3 | line-height: 0; 4 | vertical-align: middle; 5 | } 6 | 7 | .list-toolbar .pull-right { 8 | float: none; 9 | } 10 | 11 | /*.list-toolbar .clear-fix { 12 | zoom: 1; 13 | overflow:hidden; 14 | }*/ 15 | 16 | /*.form-group { 17 | height: 60px; 18 | }*/ 19 | .input-group[class*=col-] { 20 | float: left; 21 | } 22 | 23 | .input-group > .btn-group > .btn { 24 | line-height: 20px; 25 | } 26 | 27 | 28 | .page-divider { 29 | margin: 20px 0 10px 0; 30 | border: 0; 31 | border-top: 1px solid #eee; 32 | } 33 | 34 | .center { 35 | display: table; 36 | width: auto; 37 | margin-left: auto; 38 | margin-right: auto; 39 | } 40 | 41 | .border-area { 42 | position: relative; 43 | margin: 5px 0; 44 | padding: 5px 10px; 45 | background-color: #fff; 46 | border: 1px solid #ddd; 47 | -webkit-border-radius: 4px; 48 | -moz-border-radius: 4px; 49 | border-radius: 4px; 50 | } 51 | 52 | .remover { 53 | position: absolute; 54 | top: 7px; 55 | right: 7px; 56 | } 57 | 58 | .disable-mask { 59 | width: 100%; 60 | height: 100%; 61 | display: block; 62 | opacity: 0.6; 63 | position: absolute; 64 | background: #999; 65 | z-index: 1000; 66 | top: -0.5px; 67 | } 68 | 69 | .alert_placeholder { 70 | height: 45px; 71 | } 72 | 73 | .branch-selector { 74 | max-height: 300px; 75 | overflow: auto; 76 | } 77 | 78 | .keep-space { 79 | margin-left: 10px; 80 | margin-right: 10px; 81 | } 82 | 83 | .footer { 84 | padding: 0 0 10px 0; 85 | } 86 | 87 | .git-text { 88 | cursor: auto !important; 89 | } 90 | 91 | .blame-code { 92 | margin: 0 !important; 93 | padding: 0 !important; 94 | border: none !important; 95 | background: none; 96 | } 97 | 98 | .blame-info { 99 | width: 300px; 100 | } 101 | 102 | .url-path { 103 | width: 400px; 104 | } 105 | .url-path > input { 106 | background: #fff !important; 107 | } 108 | 109 | [aria-label] { 110 | position: relative; 111 | } 112 | [aria-label]:after { 113 | display: none; 114 | position: absolute; 115 | top: 100%; 116 | right: 50%; 117 | padding: 5px 8px; 118 | border-radius: 3px; 119 | background-color: rgba(0, 0, 0, 0.8); 120 | color: #fff; 121 | content: attr(aria-label); 122 | margin-top: 7px; 123 | font-size: 12px; 124 | white-space: pre; 125 | pointer-events: none; 126 | transform: translateX(50%); 127 | z-index: 10000; 128 | } 129 | 130 | [aria-label]:hover:after, [aria-label]:focus:after { 131 | display: block; 132 | } 133 | 134 | /* override bootstrap */ 135 | @media print { 136 | a[href]:after { 137 | content: none !important; 138 | } 139 | } 140 | 141 | .dl-horizontal dt { padding:5px 0; } 142 | .dl-horizontal dd { padding:5px 0; } 143 | 144 | @media (min-width: 768px) { 145 | .dl-horizontal dt { width: 240px; } 146 | .dl-horizontal dd { margin-left: 260px; } 147 | } 148 | @media (min-width: 470px) { 149 | .nav-justified > li { 150 | display: table-cell; 151 | width: 1%; 152 | } 153 | .nav-justified > li > a { 154 | margin-bottom: 0; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /GitCandy.Web/Git/ArchiverAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.IO.Compression; 3 | using System.Text; 4 | using GitCandy.Git.Cache; 5 | using GitCandy.Web; 6 | using LibGit2Sharp; 7 | using NewLife; 8 | using NewLife.Reflection; 9 | 10 | namespace GitCandy.Git; 11 | 12 | public class ArchiverAccessor : GitCacheAccessor 13 | { 14 | private readonly Commit commit; 15 | private readonly Encoding[] encodings; 16 | 17 | public ArchiverAccessor(String repoId, Repository repo, Commit commit, params Encoding[] encodings) 18 | : base(repoId, repo) 19 | { 20 | Contract.Requires(commit != null); 21 | Contract.Requires(encodings != null); 22 | 23 | this.commit = commit; 24 | this.encodings = encodings; 25 | } 26 | 27 | public override Boolean IsAsync => false; 28 | 29 | protected override String GetCacheKey() => GetCacheKey(commit.Sha); 30 | 31 | protected override void Init() 32 | { 33 | var info = new FileInfo(Path.Combine(GitSetting.Current.CachePath.GetFullPath(), GetCacheFile())); 34 | if (!info.Directory.Exists) info.Directory.Create(); 35 | 36 | _result = info.FullName; 37 | } 38 | 39 | protected override void Calculate() 40 | { 41 | using (var zip = ZipFile.Open(_result, ZipArchiveMode.Create)) 42 | { 43 | var stack = new Stack(); 44 | 45 | stack.Push(commit.Tree); 46 | while (stack.Count != 0) 47 | { 48 | var tree = stack.Pop(); 49 | foreach (var entry in tree) 50 | { 51 | switch (entry.TargetType) 52 | { 53 | case TreeEntryTargetType.Blob: 54 | { 55 | var zipEntry = zip.CreateEntry(entry.Path); 56 | var ms = zipEntry.Open(); 57 | var blob = (Blob)entry.Target; 58 | blob.GetContentStream().CopyTo(ms); 59 | ms.Close(); 60 | break; 61 | } 62 | case TreeEntryTargetType.Tree: 63 | stack.Push((Tree)entry.Target); 64 | break; 65 | case TreeEntryTargetType.GitLink: 66 | { 67 | var zipEntry = zip.CreateEntry(entry.Path + "/.gitsubmodule"); 68 | var ms = zipEntry.Open(); 69 | ms.Write(entry.Target.Sha.GetBytes()); 70 | ms.Close(); 71 | break; 72 | } 73 | } 74 | } 75 | } 76 | //zip.SetComment(commit.Sha); 77 | var sb = new StringBuilder(); 78 | sb.AppendLine(commit.Sha); 79 | sb.AppendLine(commit.Message); 80 | 81 | var au = commit.Author; 82 | if (au != null) sb.AppendFormat("{0}({1}) {2}", au.Name, au.Email, au.When.DateTime.ToFullString()); 83 | 84 | var enc = Encoding.Default; 85 | zip.SetValue("_archiveComment", sb.ToString().GetBytes(enc)); 86 | } 87 | _resultDone = true; 88 | } 89 | 90 | protected override Boolean Load() => File.Exists(_result); 91 | 92 | protected override void Save() { } 93 | } -------------------------------------------------------------------------------- /GitCandy.Web/Models/UserModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using GitCandy.Base; 3 | using GitCandy.Web.App_GlobalResources; 4 | 5 | namespace GitCandy.Models 6 | { 7 | public class UserModel 8 | { 9 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 10 | [StringLength(20, MinimumLength = 2, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 11 | [RegularExpression(RegularExpression.Username, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Name")] 12 | [Display(ResourceType = typeof(SR), Name = "Account_Username")] 13 | public String Name { get; set; } 14 | 15 | [StringLength(20, MinimumLength = 2, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLength")] 16 | [Display(ResourceType = typeof(SR), Name = "Account_Nickname")] 17 | [DisplayFormat(ConvertEmptyStringToNull = false)] 18 | public String Nickname { get; set; } 19 | 20 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 21 | [StringLength(50, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLength")] 22 | [RegularExpression(RegularExpression.Email, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Email")] 23 | [DataType(DataType.EmailAddress)] 24 | [Display(ResourceType = typeof(SR), Name = "Account_Email")] 25 | public String Email { get; set; } 26 | 27 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 28 | [StringLength(20, MinimumLength = 5, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 29 | [DataType(DataType.Password)] 30 | [Display(ResourceType = typeof(SR), Name = "Account_Password")] 31 | public String Password { get; set; } 32 | 33 | [Required(ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Required")] 34 | [StringLength(20, MinimumLength = 6, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLengthRange")] 35 | [Compare("Password", ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_Compare")] 36 | [DataType(DataType.Password)] 37 | [Display(ResourceType = typeof(SR), Name = "Account_ConformPassword")] 38 | public String ConformPassword { get; set; } 39 | 40 | [StringLength(500, ErrorMessageResourceType = typeof(SR), ErrorMessageResourceName = "Validation_StringLength")] 41 | [Display(ResourceType = typeof(SR), Name = "Account_Description")] 42 | [DisplayFormat(ConvertEmptyStringToNull = false)] 43 | public String Description { get; set; } 44 | 45 | [Display(ResourceType = typeof(SR), Name = "Account_IsSystemAdministrator")] 46 | [UIHint("YesNo")] 47 | public Boolean IsAdmin { get; set; } 48 | 49 | [Display(ResourceType = typeof(SR), Name = "Account_Teams")] 50 | [UIHint("Maps")] 51 | //[AdditionalMetadata("Controller", "Team")] 52 | public IDictionary Teams { get; set; } 53 | 54 | [Display(ResourceType = typeof(SR), Name = "Account_Repositories")] 55 | [UIHint("Members")] 56 | //[AdditionalMetadata("Controller", "Repository")] 57 | public String[] Repositories { get; set; } 58 | } 59 | } -------------------------------------------------------------------------------- /GitCandy.Web/Git/ContributorsAccessor.cs: -------------------------------------------------------------------------------- 1 | using GitCandy.Git.Cache; 2 | using GitCandy.Models; 3 | using LibGit2Sharp; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Diagnostics.Contracts; 7 | using System.Linq; 8 | 9 | namespace GitCandy.Git 10 | { 11 | public class ContributorsAccessor : GitCacheAccessor 12 | { 13 | private Commit commit; 14 | private String key; 15 | 16 | public ContributorsAccessor(String repoId, Repository repo, Commit commit) 17 | : base(repoId, repo) 18 | { 19 | Contract.Requires(commit != null); 20 | 21 | this.commit = commit; 22 | this.key = commit.Sha; 23 | } 24 | 25 | protected override String GetCacheKey() => GetCacheKey(key); 26 | 27 | protected override void Init() 28 | { 29 | _result = new RepositoryStatisticsModel.Statistics 30 | { 31 | OrderedCommits = [] 32 | }; 33 | } 34 | 35 | protected override void Calculate() 36 | { 37 | using var repo = new Repository(this.repoPath); 38 | var commit = repo.Lookup(key); 39 | var ancestors = repo.Commits 40 | .QueryBy(new CommitFilter { IncludeReachableFrom = commit }); 41 | 42 | var dict = new Dictionary(); 43 | var statistics = new RepositoryStatisticsModel.Statistics(); 44 | foreach (var ancestor in ancestors) 45 | { 46 | statistics.NumberOfCommits++; 47 | var author = ancestor.Author.ToString(); 48 | if (dict.ContainsKey(author)) 49 | { 50 | dict[author]++; 51 | } 52 | else 53 | { 54 | dict.Add(author, 1); 55 | statistics.NumberOfContributors++; 56 | } 57 | } 58 | statistics.NumberOfFiles = FilesInCommit(commit, out var size); 59 | statistics.SizeOfSource = size; 60 | 61 | var commits = dict 62 | .OrderByDescending(s => s.Value) 63 | .Select(s => new RepositoryStatisticsModel.ContributorCommits { Author = s.Key, CommitsCount = s.Value }) 64 | .ToArray(); 65 | 66 | statistics.OrderedCommits = commits; 67 | 68 | _result = statistics; 69 | _resultDone = true; 70 | } 71 | 72 | private Int32 FilesInCommit(Commit commit, out Int64 sourceSize) 73 | { 74 | var count = 0; 75 | var stack = new Stack(); 76 | sourceSize = 0; 77 | 78 | var repo = ((IBelongToARepository)commit).Repository; 79 | 80 | stack.Push(commit.Tree); 81 | while (stack.Count != 0) 82 | { 83 | var tree = stack.Pop(); 84 | foreach (var entry in tree) 85 | switch (entry.TargetType) 86 | { 87 | case TreeEntryTargetType.Blob: 88 | count++; 89 | sourceSize += repo.ObjectDatabase.RetrieveObjectMetadata(entry.Target.Id).Size; 90 | break; 91 | case TreeEntryTargetType.Tree: 92 | stack.Push((Tree)entry.Target); 93 | break; 94 | } 95 | } 96 | return count; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /GitCandy.Web/Views/Repository/Create.cshtml: -------------------------------------------------------------------------------- 1 | @model GitCandy.Models.RepositoryModel 2 | 3 | @{ 4 | ViewBag.Title = String.Format(SR.Shared_TitleFormat, SR.Repository_CreateTitle); 5 | var owners = ViewBag.Owners as IDictionary; 6 | } 7 | 8 |

@SR.Repository_CreateTitle

9 | 10 | 11 | @using (Html.BeginForm("Create", "Repository", FormMethod.Post)) 12 | { 13 |
14 | 15 |
@Html.DisplayNameFor(s => s.Owner)
16 |
@Html.DropDownListFor(s => s.Owner, owners.Select(e => new SelectListItem { Value = e.Key, Text = e.Value }), new { @class = "form-control" })
17 |
18 | 19 | @Html.ValidationMessageFor(s => s.Owner) 20 | 21 |
22 | 23 |
@Html.DisplayNameFor(s => s.Name)
24 |
@Html.TextBoxFor(s => s.Name, new { @class = "form-control" })
25 |
26 | 27 | @Html.ValidationMessageFor(s => s.Name) 28 | 29 |
30 | 31 |
@Html.DisplayNameFor(s => s.IsPrivate)
32 |
33 |
34 | @Html.CheckBoxFor(s => s.IsPrivate, new { data_size = "small" }) 35 |
36 |
37 | 38 |
@Html.DisplayNameFor(s => s.AllowAnonymousRead)
39 |
40 |
41 | @Html.CheckBoxFor(s => s.AllowAnonymousRead, new { data_size = "small" }) 42 |
43 |
44 | 45 |
@Html.DisplayNameFor(s => s.AllowAnonymousWrite)
46 |
47 |
48 | @Html.CheckBoxFor(s => s.AllowAnonymousWrite, new { data_size = "small" }) 49 |
50 |
51 |
@Html.DisplayNameFor(s => s.HowInit)
52 |
53 |
54 | 57 | 60 |
61 | @Html.TextBoxFor(s => s.HowInit, new { @class = "hide" }) 62 |
63 |
64 |
@Html.DisplayNameFor(s => s.RemoteUrl)
65 | @Html.TextBoxFor(s => s.RemoteUrl, new { @class = "form-control", placeholder = "http(s)://" }) 66 |

@SR.Repository_ImportTips

67 |
68 |
69 |
70 | 71 |
@Html.DisplayNameFor(s => s.Description)
72 |
@Html.TextAreaFor(s => s.Description, 4, 0, new { @class = "form-control" })
73 |
74 | 75 | @Html.ValidationMessageFor(s => s.Description) 76 | 77 |
78 | 79 |
80 |
@Html.ValidationSummary(false, null, new { @class = "alert alert-dismissable alert-danger" })
81 | 82 |
83 |
84 |   85 |   86 |
87 |
88 | } 89 | -------------------------------------------------------------------------------- /GitCandy/Entity/Entity/SSH密钥.Biz.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * XCoder v6.8.6160.27608 3 | * 作者:Stone/X2 4 | * 时间:2016-11-21 15:48:51 5 | * 版权:版权所有 (C) 新生命开发团队 2002~2016 6 | */ 7 | using System; 8 | using System.Collections.Generic; 9 | using System.ComponentModel; 10 | using System.Text; 11 | using System.Xml.Serialization; 12 | using NewLife.Log; 13 | using NewLife.Web; 14 | using NewLife.Data; 15 | using XCode; 16 | using XCode.Configuration; 17 | using XCode.Membership; 18 | 19 | namespace NewLife.GitCandy.Entity 20 | { 21 | /// SSH密钥 22 | public partial class SshKey : LogEntity 23 | { 24 | #region 对象操作 25 | #endregion 26 | 27 | #region 扩展属性 28 | private User _User; 29 | /// 团队 30 | public User User 31 | { 32 | get 33 | { 34 | //if (_User == null && GatewayID > 0 && !Dirtys.ContainsKey("User")) 35 | { 36 | _User = User.FindByID(UserID); 37 | //Dirtys["User"] = true; 38 | } 39 | return _User; 40 | } 41 | set { _User = value; } 42 | } 43 | 44 | /// 用户名称 45 | [DisplayName("用户")] 46 | [Map(__.UserID, typeof(User), "ID")] 47 | public String UserName { get { return User + ""; } } 48 | #endregion 49 | 50 | #region 扩展查询 51 | 52 | /// 根据用户查找 53 | /// 用户 54 | /// 55 | [DataObjectMethod(DataObjectMethodType.Select, false)] 56 | public static SshKey FindByUserID(Int32 userid) 57 | { 58 | if (userid <= 0) return null; 59 | 60 | if (Meta.Count >= 1000) 61 | return Find(__.UserID, userid); 62 | else // 实体缓存 63 | return Meta.Cache.Entities.Find(e => e.UserID == userid); 64 | // 单对象缓存 65 | //return Meta.SingleCache[userid]; 66 | } 67 | 68 | public static IList FindAllByUserID(Int32 userid) 69 | { 70 | if (userid <= 0) return new List(); 71 | 72 | if (Meta.Count >= 1000) 73 | return FindAll(_.UserID == userid); 74 | else 75 | return Meta.Cache.Entities.FindAll(e => e.UserID == userid); 76 | } 77 | 78 | public static SshKey FindByFingerprint(String fingerprint) 79 | { 80 | return Find(__.Fingerprint, fingerprint); 81 | } 82 | #endregion 83 | 84 | #region 高级查询 85 | // 以下为自定义高级查询的例子 86 | 87 | /// 查询满足条件的记录集,分页、排序 88 | /// 用户编号 89 | /// 开始时间 90 | /// 结束时间 91 | /// 关键字 92 | /// 分页排序参数,同时返回满足条件的总记录数 93 | /// 实体集 94 | public static IList Search(Int32 userid, DateTime start, DateTime end, String key, PageParameter param) 95 | { 96 | // WhereExpression重载&和|运算符,作为And和Or的替代 97 | // SearchWhereByKeys系列方法用于构建针对字符串字段的模糊搜索,第二个参数可指定要搜索的字段 98 | var exp = SearchWhereByKeys(key, null, null); 99 | 100 | // 以下仅为演示,Field(继承自FieldItem)重载了==、!=、>、<、>=、<=等运算符 101 | //if (userid > 0) exp &= _.OperatorID == userid; 102 | //if (isSign != null) exp &= _.IsSign == isSign.Value; 103 | //exp &= _.OccurTime.Between(start, end); // 大于等于start,小于end,当start/end大于MinValue时有效 104 | 105 | return FindAll(exp, param); 106 | } 107 | #endregion 108 | 109 | #region 扩展操作 110 | #endregion 111 | 112 | #region 业务 113 | #endregion 114 | } 115 | } -------------------------------------------------------------------------------- /GitCandy.Web/wwwroot/Content/Cube.css: -------------------------------------------------------------------------------- 1 | @media(max-width: 768px) { 2 | body { padding-left: 0px; padding-right: 0px; } 3 | 4 | .page-content { padding: 0px; } 5 | 6 | .form-horizontal .control-label { float: left; } 7 | 8 | .form-group { display: inline-block; margin-bottom: 2px; } 9 | 10 | .form-inline .form-group { display: inline-block; margin-bottom: 0; vertical-align: middle; } 11 | 12 | .form-inline .control-label { margin-bottom: 0; vertical-align: middle; } 13 | 14 | /*.form-inline .input-group { 15 | display: inline-block; 16 | vertical-align: middle; 17 | } 18 | 19 | .form-inline .input-group-addon { 20 | display: inline-table; 21 | vertical-align: middle; 22 | float: left; 23 | }*/ 24 | 25 | .form-inline .input-group { display: inline-table; vertical-align: middle; } 26 | } 27 | 28 | .list-toolbar .btn-sm-force, .btn-toolbar .btn-sm-force { height: 33px; line-height: 0; vertical-align: middle; } 29 | 30 | .list-toolbar .pull-right { float: none; } 31 | 32 | /*.list-toolbar .clear-fix { 33 | zoom: 1; 34 | overflow:hidden; 35 | }*/ 36 | 37 | /*.form-group { 38 | height: 60px; 39 | }*/ 40 | .input-group[class*=col-] { float: left; } 41 | 42 | .input-group > .btn-group > .btn { line-height: 20px; } 43 | 44 | /*************** cube-login 2017-09-03 新版本样式*****************/ 45 | .login-logo { width: 130px; font-size: 130px; color: #4CA6FF;margin-top:50px; } 46 | .cube-login { background: #fff; padding-bottom: 40px; border-radius: 15px; text-align: center; } 47 | .cube-login .heading { display: block; font-size: 24px; font-weight: 700; padding: 5px 0; margin-bottom: 20px; } 48 | .cube-login .form-group { padding: 0 40px; margin: 0 0 25px 0; position: relative;display:block; } 49 | .cube-login .form-control { border-radius: 20px; box-shadow: none; padding: 0 20px 0 45px; height: 40px; transition: all 0.3s ease 0s; } 50 | .cube-login .form-control:focus { background: #e0e0e0; box-shadow: none; outline: 0 none; } 51 | .cube-login .form-group i { position: absolute; top: 12px; left: 60px; font-size: 17px; color: #c8c8c8; transition: all 0.5s ease 0s; } 52 | .cube-login .form-group a { position: absolute; top: 12px; right: 0px; font-size: 17px; color: #c8c8c8; transition: all 0.5s ease 0s; color: #4CA6FF; } 53 | .cube-login .form-control:focus + i { color: #00b4ef; } 54 | .cube-login .fa-question-circle { display: inline-block; position: absolute; top: 12px; right: 60px; font-size: 20px; color: #808080; transition: all 0.5s ease 0s; } 55 | .cube-login .fa-question-circle:hover { color: #000; } 56 | .cube-login .main-checkbox { float: left; width: 20px; height: 20px; background: #11a3fc; border-radius: 50%; position: relative; margin: 5px 0 0 5px; border: 1px solid #11a3fc; } 57 | .cube-login .main-checkbox label { width: 20px; height: 20px; position: absolute; top: 0; left: 0; cursor: pointer; } 58 | .cube-login .main-checkbox label:after { content: ""; width: 10px; height: 5px; position: absolute; top: 5px; left: 4px; border: 3px solid #fff; border-top: none; border-right: none; background: transparent; opacity: 0; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); } 59 | .cube-login .main-checkbox input[type=checkbox] { visibility: hidden; } 60 | .cube-login .main-checkbox input[type=checkbox]:checked + label:after { opacity: 1; } 61 | .cube-login .text { float: left; margin-left: 7px; line-height: 20px; padding-top: 5px; text-transform: capitalize; } 62 | .cube-login .btn { float: right; font-size: 14px; color: #fff; background: #00b4ef; border-radius: 30px; padding: 8px 50px; border: none; text-transform: capitalize; transition: all 0.5s ease 0s; } 63 | 64 | /* 2019-03-15 表格禁止折行,避免拥挤 */ 65 | .table-responsive > .table > tbody > tr > td, 66 | .table-responsive > .table > tbody > tr > th, 67 | .table-responsive > .table > tfoot > tr > td, 68 | .table-responsive > .table > tfoot > tr > th, 69 | .table-responsive > .table > thead > tr > td, 70 | .table-responsive > .table > thead > tr > th { 71 | white-space: nowrap 72 | } 73 | -------------------------------------------------------------------------------- /GitCandy/Entity/Entity/认证日志.Biz.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * XCoder v6.8.6160.27608 3 | * 作者:Stone/X2 4 | * 时间:2016-11-21 15:48:51 5 | * 版权:版权所有 (C) 新生命开发团队 2002~2016 6 | */ 7 | using System; 8 | using System.Collections.Generic; 9 | using System.ComponentModel; 10 | using System.Text; 11 | using System.Xml.Serialization; 12 | using NewLife.Log; 13 | using NewLife.Web; 14 | using NewLife.Data; 15 | using XCode; 16 | using XCode.Configuration; 17 | using XCode.Membership; 18 | using System.Linq; 19 | 20 | namespace NewLife.GitCandy.Entity 21 | { 22 | /// 认证日志 23 | public partial class AuthorizationLog : Entity 24 | { 25 | #region 对象操作 26 | #endregion 27 | 28 | #region 扩展属性 29 | private User _User; 30 | /// 团队 31 | public User User 32 | { 33 | get 34 | { 35 | //if (_User == null && GatewayID > 0 && !Dirtys.ContainsKey("User")) 36 | { 37 | _User = User.FindByID(UserID); 38 | //Dirtys["User"] = true; 39 | } 40 | return _User; 41 | } 42 | set { _User = value; } 43 | } 44 | 45 | /// 用户名称 46 | [DisplayName("用户")] 47 | [Map(__.UserID, typeof(User), "ID")] 48 | public String UserName { get { return User + ""; } } 49 | #endregion 50 | 51 | #region 扩展查询 52 | 53 | /// 根据认证码查找 54 | /// 认证码 55 | /// 56 | [DataObjectMethod(DataObjectMethodType.Select, false)] 57 | public static AuthorizationLog FindByAuthCode(String authcode) 58 | { 59 | if (authcode.IsNullOrEmpty()) return null; 60 | 61 | if (Meta.Count >= 1000) 62 | return Find(__.AuthCode, authcode); 63 | else // 实体缓存 64 | return Meta.Cache.Entities.FirstOrDefault(e => e.AuthCode == authcode); 65 | // 单对象缓存 66 | //return Meta.SingleCache[authcode]; 67 | } 68 | 69 | /// 根据用户查找 70 | /// 用户 71 | /// 72 | [DataObjectMethod(DataObjectMethodType.Select, false)] 73 | public static IList FindAllByUserID(Int32 userid) 74 | { 75 | if (userid <= 0) return new List(); 76 | 77 | if (Meta.Count >= 1000) 78 | return FindAll(_.UserID == userid); 79 | else // 实体缓存 80 | return Meta.Cache.Entities.Where(e => e.UserID == userid).ToList(); 81 | // 单对象缓存 82 | //return Meta.SingleCache[userid]; 83 | } 84 | #endregion 85 | 86 | #region 高级查询 87 | // 以下为自定义高级查询的例子 88 | 89 | /// 查询满足条件的记录集,分页、排序 90 | /// 用户编号 91 | /// 开始时间 92 | /// 结束时间 93 | /// 关键字 94 | /// 分页排序参数,同时返回满足条件的总记录数 95 | /// 实体集 96 | public static IList Search(Int32 userid, DateTime start, DateTime end, String key, PageParameter param) 97 | { 98 | // WhereExpression重载&和|运算符,作为And和Or的替代 99 | // SearchWhereByKeys系列方法用于构建针对字符串字段的模糊搜索,第二个参数可指定要搜索的字段 100 | var exp = SearchWhereByKeys(key, null, null); 101 | 102 | // 以下仅为演示,Field(继承自FieldItem)重载了==、!=、>、<、>=、<=等运算符 103 | //if (userid > 0) exp &= _.OperatorID == userid; 104 | //if (isSign != null) exp &= _.IsSign == isSign.Value; 105 | //exp &= _.OccurTime.Between(start, end); // 大于等于start,小于end,当start/end大于MinValue时有效 106 | 107 | return FindAll(exp, param); 108 | } 109 | #endregion 110 | 111 | #region 扩展操作 112 | #endregion 113 | 114 | #region 业务 115 | #endregion 116 | } 117 | } -------------------------------------------------------------------------------- /GitCandy/Base/Pager.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Pager for ASP.NET MVC 3 | * source : http://www.superstarcoders.com/blogs/posts/pager-for-asp-net-mvc.aspx 4 | */ 5 | 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Collections; 10 | 11 | namespace GitCandy.Base; 12 | 13 | /// 分页类 14 | public sealed class Pager : IEnumerable 15 | { 16 | private Int32 _numberOfPages; 17 | private Int32 _skipPages; 18 | private Int32 _takePages; 19 | private Int32 _currentPageIndex; 20 | private Int32 _numberOfItems; 21 | private Int32 _itemsPerPage; 22 | 23 | private Pager() 24 | { 25 | } 26 | 27 | private Pager(Pager pager) 28 | { 29 | _numberOfItems = pager._numberOfItems; 30 | _currentPageIndex = pager._currentPageIndex; 31 | _numberOfPages = pager._numberOfPages; 32 | _takePages = pager._takePages; 33 | _skipPages = pager._skipPages; 34 | _itemsPerPage = pager._itemsPerPage; 35 | } 36 | 37 | /// 38 | /// Creates a pager for the given number of items. 39 | /// 40 | public static Pager Items(Int32 numberOfItems) 41 | { 42 | return new Pager 43 | { 44 | _numberOfItems = numberOfItems, 45 | _currentPageIndex = 1, 46 | _numberOfPages = 1, 47 | _skipPages = 0, 48 | _takePages = 1, 49 | _itemsPerPage = numberOfItems 50 | }; 51 | } 52 | 53 | /// 54 | /// Specifies the number of items per page. 55 | /// 56 | public Pager PerPage(Int32 itemsPerPage) 57 | { 58 | var numberOfPages = (_numberOfItems + itemsPerPage - 1) / itemsPerPage; 59 | 60 | return new Pager(this) 61 | { 62 | _numberOfPages = numberOfPages, 63 | _skipPages = 0, 64 | _takePages = numberOfPages - _currentPageIndex + 1, 65 | _itemsPerPage = itemsPerPage 66 | }; 67 | } 68 | 69 | /// 70 | /// Moves the pager to the given page index 71 | /// 72 | public Pager Move(Int32 pageIndex) 73 | { 74 | return new Pager(this) 75 | { 76 | _currentPageIndex = pageIndex 77 | }; 78 | } 79 | 80 | /// 81 | /// Segments the pager so that it will display a maximum number of pages. 82 | /// 83 | public Pager Segment(Int32 maximum) 84 | { 85 | var count = Math.Min(_numberOfPages, maximum); 86 | 87 | return new Pager(this) 88 | { 89 | _takePages = count, 90 | _skipPages = Math.Min(_skipPages, _numberOfPages - count), 91 | }; 92 | } 93 | 94 | /// 95 | /// Centers the segment around the current page 96 | /// 97 | /// 98 | public Pager Center() 99 | { 100 | var radius = ((_takePages + 1) / 2); 101 | 102 | return new Pager(this) 103 | { 104 | _skipPages = Math.Min(Math.Max(_currentPageIndex - radius, 0), _numberOfPages - _takePages) 105 | }; 106 | } 107 | 108 | public IEnumerator GetEnumerator() => Enumerable.Range(_skipPages + 1, _takePages).GetEnumerator(); 109 | 110 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 111 | 112 | public Boolean IsPaged => _numberOfItems > _itemsPerPage; 113 | 114 | public Int32 NumberOfPages => _numberOfPages; 115 | 116 | public Boolean IsUnpaged => _numberOfPages == 1; 117 | 118 | public Int32 CurrentPageIndex => _currentPageIndex; 119 | 120 | public Int32 NextPageIndex => _currentPageIndex + 1; 121 | 122 | public Int32 LastPageIndex => _numberOfPages; 123 | 124 | public Int32 FirstPageIndex => 1; 125 | 126 | public Boolean HasNextPage => _currentPageIndex < _numberOfPages && _numberOfPages > 1; 127 | 128 | public Boolean HasPreviousPage => _currentPageIndex > 1 && _numberOfPages > 1; 129 | 130 | public Int32 PreviousPageIndex => _currentPageIndex - 1; 131 | 132 | public Boolean IsSegmented => _skipPages > 0 || _skipPages + 1 + _takePages < _numberOfPages; 133 | 134 | public Boolean IsEmpty => _numberOfPages < 1; 135 | 136 | public Boolean IsFirstSegment => _skipPages == 0; 137 | 138 | public Boolean IsLastSegment => _skipPages + 1 + _takePages >= _numberOfPages; 139 | } -------------------------------------------------------------------------------- /GitCandy/Entity/Entity/用户仓库.Biz.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * XCoder v6.8.6160.27608 3 | * 作者:Stone/X2 4 | * 时间:2016-11-21 15:48:51 5 | * 版权:版权所有 (C) 新生命开发团队 2002~2016 6 | */ 7 | using System; 8 | using System.Collections.Generic; 9 | using System.ComponentModel; 10 | using System.Linq; 11 | using System.Runtime.Serialization; 12 | using System.Xml.Serialization; 13 | using NewLife.Data; 14 | using XCode; 15 | using XCode.DataAccessLayer; 16 | using XCode.Membership; 17 | 18 | namespace NewLife.GitCandy.Entity; 19 | 20 | /// 用户仓库 21 | public partial class UserRepository : LogEntity 22 | { 23 | #region 对象操作 24 | static UserRepository() 25 | { 26 | Meta.Modules.Add(); 27 | Meta.Modules.Add(); 28 | Meta.Modules.Add(); 29 | } 30 | 31 | public override void Valid(Boolean isNew) 32 | { 33 | if (UserID <= 0) throw new ArgumentNullException(__.UserID, _.UserID.DisplayName); 34 | if (RepositoryID <= 0) throw new ArgumentNullException(__.RepositoryID, _.RepositoryID.DisplayName); 35 | 36 | base.Valid(isNew); 37 | } 38 | #endregion 39 | 40 | #region 扩展属性 41 | /// 团队 42 | [XmlIgnore, IgnoreDataMember] 43 | public User User => Extends.Get(nameof(User), k => User.FindByID(UserID)); 44 | 45 | /// 用户名称 46 | [DisplayName("用户")] 47 | [Map(__.UserID, typeof(User), "ID")] 48 | public String UserName => User + ""; 49 | 50 | /// 仓库 51 | [XmlIgnore, IgnoreDataMember] 52 | public Repository Repository => Extends.Get(nameof(Repository), k => Repository.FindByID(RepositoryID)); 53 | 54 | /// 仓库名称 55 | [DisplayName("仓库")] 56 | [Map(__.RepositoryID, typeof(Repository), "ID")] 57 | public String RepositoryName => Repository + ""; 58 | #endregion 59 | 60 | #region 扩展查询 61 | 62 | /// 根据用户、仓库查找 63 | /// 用户 64 | /// 仓库 65 | /// 66 | [DataObjectMethod(DataObjectMethodType.Select, false)] 67 | public static UserRepository FindByUserIDAndRepositoryID(Int32 userid, Int32 repositoryid) 68 | { 69 | if (Meta.Count >= 1000) 70 | return Find(new String[] { __.UserID, __.RepositoryID }, new Object[] { userid, repositoryid }); 71 | else // 实体缓存 72 | return Meta.Cache.Entities.FirstOrDefault(e => e.UserID == userid && e.RepositoryID == repositoryid); 73 | } 74 | 75 | public static IList FindAllByUserID(Int32 userid) 76 | { 77 | if (userid <= 0) return new List(); 78 | 79 | if (Meta.Count >= 1000) 80 | return FindAll(_.UserID == userid); 81 | else 82 | return Meta.Cache.Entities.Where(e => e.UserID == userid).ToList(); 83 | } 84 | 85 | public static IList FindAllByRepositoryID(Int32 repid) 86 | { 87 | if (repid <= 0) return new List(); 88 | 89 | if (Meta.Count >= 1000) 90 | return FindAll(_.RepositoryID == repid); 91 | else 92 | return Meta.Cache.Entities.Where(e => e.RepositoryID == repid).ToList(); 93 | } 94 | #endregion 95 | 96 | #region 高级查询 97 | // 以下为自定义高级查询的例子 98 | 99 | /// 查询满足条件的记录集,分页、排序 100 | /// 用户编号 101 | /// 开始时间 102 | /// 结束时间 103 | /// 关键字 104 | /// 分页排序参数,同时返回满足条件的总记录数 105 | /// 实体集 106 | public static IList Search(Int32 userid, DateTime start, DateTime end, String key, PageParameter param) 107 | { 108 | // WhereExpression重载&和|运算符,作为And和Or的替代 109 | // SearchWhereByKeys系列方法用于构建针对字符串字段的模糊搜索,第二个参数可指定要搜索的字段 110 | var exp = SearchWhereByKeys(key, null, null); 111 | 112 | if (userid > 0) exp &= _.UserID == userid; 113 | exp &= _.UpdateTime.Between(start, end); 114 | 115 | return FindAll(exp, param); 116 | } 117 | 118 | public static SelectBuilder SearchSql(IList userIds) => FindSQL(_.UserID.In(userIds), null, _.RepositoryID); 119 | #endregion 120 | 121 | #region 扩展操作 122 | #endregion 123 | 124 | #region 业务 125 | public override String ToString() => $"{UserName},{RepositoryName}"; 126 | #endregion 127 | } -------------------------------------------------------------------------------- /GitCandy.Web/Git/SummaryAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Linq; 4 | using GitCandy.Base; 5 | using GitCandy.Git.Cache; 6 | using GitCandy.Web.Extensions; 7 | using LibGit2Sharp; 8 | 9 | namespace GitCandy.Git 10 | { 11 | public class SummaryAccessor : GitCacheAccessor 12 | { 13 | private readonly Commit commit; 14 | private readonly Tree tree; 15 | 16 | public SummaryAccessor(String repoId, Repository repo, Commit commit, Tree tree) 17 | : base(repoId, repo) 18 | { 19 | Contract.Requires(commit != null); 20 | Contract.Requires(tree != null); 21 | 22 | this.commit = commit; 23 | this.tree = tree; 24 | } 25 | 26 | protected override String GetCacheKey() => GetCacheKey(commit.Id, tree.Id); 27 | 28 | protected override void Init() 29 | { 30 | _result = tree 31 | .OrderBy(s => s.TargetType == TreeEntryTargetType.Blob) 32 | .ThenBy(s => s.Name, new StringLogicalComparer()) 33 | .Select(s => new RevisionSummaryCacheItem 34 | { 35 | Name = s.Name, 36 | Path = s.Path.Replace('\\', '/'), 37 | TargetSha = s.Target.Sha, 38 | MessageShort = "Loading...", 39 | AuthorEmail = "Loading...", 40 | AuthorName = "Loading...", 41 | CommitterEmail = "Loading...", 42 | CommitterName = "Loading...", 43 | }) 44 | .ToArray(); 45 | } 46 | 47 | protected override void Calculate() 48 | { 49 | using (var repo = new Repository(this.repoPath)) 50 | { 51 | var ancestors = repo.Commits 52 | .QueryBy(new CommitFilter { IncludeReachableFrom = commit, SortBy = CommitSortStrategies.Topological }); 53 | 54 | // null, continue search current reference 55 | // true, have found, done 56 | // false, search has been interrupted, but waiting for next match 57 | var status = new Boolean?[_result.Length]; 58 | var done = _result.Length; 59 | Commit lastCommit = null; 60 | foreach (var ancestor in ancestors) 61 | { 62 | for (var index = 0; index < _result.Length; index++) 63 | { 64 | if (status[index] == true) 65 | continue; 66 | var item = _result[index]; 67 | var ancestorEntry = ancestor[item.Path]; 68 | if (ancestorEntry != null && ancestorEntry.Target.Sha == item.TargetSha) 69 | { 70 | item.CommitSha = ancestor.Sha; 71 | item.MessageShort = ancestor.MessageShort.RepetitionIfEmpty(GitService.UnknowString); 72 | item.AuthorEmail = ancestor.Author.Email; 73 | item.AuthorName = ancestor.Author.Name; 74 | item.AuthorWhen = ancestor.Author.When; 75 | item.CommitterEmail = ancestor.Committer.Email; 76 | item.CommitterName = ancestor.Committer.Name; 77 | item.CommitterWhen = ancestor.Committer.When; 78 | 79 | status[index] = null; 80 | } 81 | else if (status[index] == null) 82 | { 83 | var over = true; 84 | foreach (var parent in lastCommit.Parents) // Backtracking 85 | { 86 | if (parent.Sha == ancestor.Sha) 87 | continue; 88 | var entry = parent[item.Path]; 89 | if (entry != null && entry.Target.Sha == item.TargetSha) 90 | { 91 | over = false; 92 | break; 93 | } 94 | } 95 | status[index] = over; 96 | if (over) 97 | done--; 98 | } 99 | } 100 | if (done == 0) 101 | break; 102 | lastCommit = ancestor; 103 | } 104 | } 105 | _resultDone = true; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /GitCandy.Web/GitSetting.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using NewLife; 3 | using NewLife.Configuration; 4 | 5 | namespace GitCandy.Web; 6 | 7 | [Config("Git")] 8 | public class GitSetting : Config 9 | { 10 | #region 属性 11 | /// 开放服务 12 | [DisplayName("开放服务")] 13 | public Boolean IsPublicServer { get; set; } = true; 14 | 15 | /// 允许注册 16 | [DisplayName("允许注册")] 17 | public Boolean AllowRegisterUser { get; set; } = true; 18 | 19 | /// 允许创建代码库 20 | [DisplayName("允许创建代码库")] 21 | public Boolean AllowRepositoryCreation { get; set; } = true; 22 | 23 | /// 代码库存储路径 24 | [DisplayName("代码库存储路径")] 25 | public String RepositoryPath { get; set; } = "..\\Repos"; 26 | 27 | /// 缓存路径 28 | [DisplayName("缓存路径")] 29 | public String CachePath { get; set; } = "..\\Cache"; 30 | 31 | /// Git-Core路径 32 | [DisplayName("Git-Core路径")] 33 | public String GitCorePath { get; set; } 34 | 35 | /// 每页提交数 36 | [DisplayName("每页提交数")] 37 | public Int32 Commits { get; set; } = 30; 38 | 39 | /// 分页大小 40 | [DisplayName("分页大小")] 41 | public Int32 PageSize { get; set; } = 30; 42 | 43 | /// 显示参与者数 44 | [DisplayName("显示参与者数")] 45 | public Int32 Contributors { get; set; } = 50; 46 | 47 | /// 允许打包。默认true 48 | [DisplayName("允许打包。默认true")] 49 | public Boolean AllowArchive { get; set; } = true; 50 | 51 | /// 允许查看审阅。默认true 52 | [DisplayName("允许查看审阅。默认true")] 53 | public Boolean AllowBlame { get; set; } = true; 54 | 55 | /// 允许查看提交。默认true 56 | [DisplayName("允许查看提交。默认true")] 57 | public Boolean AllowCommits { get; set; } = true; 58 | 59 | /// 允许查看贡献者。默认true 60 | [DisplayName("允许查看贡献者。默认true")] 61 | public Boolean AllowContributors { get; set; } = true; 62 | 63 | /// 允许查看分支差异。默认true 64 | [DisplayName("允许查看分支差异。默认true")] 65 | public Boolean AllowHistoryDivergence { get; set; } = true; 66 | 67 | /// 允许查看摘要。默认true 68 | [DisplayName("允许查看摘要。默认true")] 69 | public Boolean AllowSummary { get; set; } = true; 70 | #endregion 71 | 72 | protected override void OnLoaded() 73 | { 74 | if (GitCorePath.IsNullOrEmpty()) GitCorePath = GetGitCore(); 75 | 76 | base.OnLoaded(); 77 | } 78 | 79 | private String GetGitCore() 80 | { 81 | var list = new List(); 82 | var variable = Environment.GetEnvironmentVariable("path"); 83 | if (variable != null) 84 | list.AddRange(variable.Split(';')); 85 | 86 | list.Add(Environment.GetEnvironmentVariable("ProgramW6432")); 87 | list.Add(Environment.GetEnvironmentVariable("ProgramFiles")); 88 | 89 | foreach (var drive in Environment.GetLogicalDrives()) 90 | { 91 | list.Add(drive + @"Program Files\Git"); 92 | list.Add(drive + @"Program Files (x86)\Git"); 93 | list.Add(drive + @"Program Files\PortableGit"); 94 | list.Add(drive + @"Program Files (x86)\PortableGit"); 95 | list.Add(drive + @"PortableGit"); 96 | } 97 | 98 | list = list.Where(x => !String.IsNullOrEmpty(x)).Distinct().ToList(); 99 | foreach (var path in list) 100 | { 101 | var ret = SearchPath(path); 102 | if (ret != null) 103 | return ret; 104 | } 105 | 106 | if (Runtime.Linux) return "/usr/bin"; 107 | 108 | return ""; 109 | } 110 | 111 | private String SearchPath(String path) 112 | { 113 | var patterns = new[] { 114 | @"..\libexec\git-core", // git 1.x 115 | @"libexec\git-core", // git 1.x 116 | @"..\mingw64\libexec\git-core", // git 2.x 117 | @"mingw64\libexec\git-core", // git 2.x 118 | }; 119 | foreach (var pattern in patterns) 120 | { 121 | var fullpath = new DirectoryInfo(Path.Combine(path, pattern)).FullName; 122 | if (File.Exists(Path.Combine(fullpath, "git.exe")) 123 | && File.Exists(Path.Combine(fullpath, "git-receive-pack.exe")) 124 | && File.Exists(Path.Combine(fullpath, "git-upload-archive.exe")) 125 | && File.Exists(Path.Combine(fullpath, "git-upload-pack.exe"))) 126 | return fullpath; 127 | } 128 | return null; 129 | } 130 | 131 | public String GetGitFile() 132 | { 133 | var path = GitCorePath; 134 | if (Runtime.Windows) return path.CombinePath("git.exe"); 135 | 136 | return path.CombinePath("git"); 137 | } 138 | } -------------------------------------------------------------------------------- /GitCandy.Web/Views/Shared/_FrontLayout.cshtml: -------------------------------------------------------------------------------- 1 | @using GitCandy.Web.App_GlobalResources 2 | @using NewLife 3 | @using NewLife.Model; 4 | @using GitCandy.Web.Extensions; 5 | @using NewLife.Web 6 | @{ 7 | var token = ViewBag.Token as IManageUser; 8 | var cfg = NewLife.Common.SysConfig.Current; 9 | var title = ViewBag.Title + ""; 10 | if (title != "" && !title.EndsWith(" - ")) { title += " - "; } 11 | title += cfg.DisplayName; 12 | var title2 = ViewBag.Title2 as String; 13 | if (!title2.IsNullOrEmpty()) { title += " - " + title2; } 14 | 15 | var retUrl = ViewContext.HttpContext.Request.GetRawUrl()?.PathAndQuery; 16 | } 17 | 18 | 19 | 20 | 21 | 22 | @title 23 | 24 | 25 | 26 | 27 | 28 | 29 | 70 | 71 |
72 | @RenderBody() 73 |
74 | 75 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | @RenderSection("scripts", required: false) 98 | 99 | 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | 214 | # To be ignored 215 | CacheVersion 216 | Information 217 | /GitCandy/Config 218 | /Data 219 | /GitCandy/Content/Cube.js 220 | /GitCandy/Content/ace 221 | /GitCandy/Content/bootstrap 222 | /GitCandy/Content/bootstrap-switch 223 | /GitCandy/Content/DateTimePicker 224 | /GitCandy/Content/artDialog 225 | /GitCandy/Content/js 226 | /Cache 227 | /GitCandy/Plugins 228 | /Repos 229 | /GitCandy.Web/Config 230 | /GitCandy.Web/Content/js 231 | /GitCandy.Web/Content/DateTimePicker 232 | /GitCandy.Web/Content/Cube.js 233 | /GitCandy.Web/Content/bootstrap-switch 234 | /GitCandy.Web/Content/bootstrap 235 | /GitCandy.Web/Content/artDialog 236 | /GitCandy.Web/Content/ace 237 | /GitCandy.Web/Plugins 238 | /GitCandy/Entity/set.config 239 | /GitCandy/Entity/Config 240 | /Avatars 241 | /GitCandy.Web/Content/Site.css 242 | /DLL 243 | /GitCandy.Web/Content/images/logo 244 | /BinTest 245 | -------------------------------------------------------------------------------- /GitCandy/Base/StringLogicalComparer.cs: -------------------------------------------------------------------------------- 1 | //(c) Vasian Cepa 2005 2 | // Version 2 3 | // 4 | // http://www.codeproject.com/Articles/11016/Numeric-String-Sort-in-C 5 | // with a bit of modification 6 | 7 | using System; 8 | using System.Collections; 9 | using System.Collections.Generic; 10 | 11 | namespace GitCandy.Base 12 | { 13 | // emulates StrCmpLogicalW, but not fully 14 | public class StringLogicalComparer : IComparer, IComparer 15 | { 16 | public Int32 Compare(Object s1, Object s2) => Compare(s1 as String, s2 as String); 17 | 18 | public Int32 Compare(String s1, String s2) 19 | { 20 | //get rid of special cases 21 | if (ReferenceEquals(s1, s2)) return 0; 22 | else if ((s1 == null) && (s2 == null)) return 0; 23 | else if (s1 == null) return -1; 24 | else if (s2 == null) return 1; 25 | 26 | if ((s1.Equals(String.Empty) && (s2.Equals(String.Empty)))) return 0; 27 | else if (s1.Equals(String.Empty)) return -1; 28 | else if (s2.Equals(String.Empty)) return -1; 29 | 30 | //WE style, special case 31 | var sp1 = Char.IsLetterOrDigit(s1, 0); 32 | var sp2 = Char.IsLetterOrDigit(s2, 0); 33 | if (sp1 && !sp2) return 1; 34 | if (!sp1 && sp2) return -1; 35 | 36 | Int32 i1 = 0, i2 = 0; //current index 37 | var r = 0; // temp result 38 | while (true) 39 | { 40 | var c1 = Char.IsDigit(s1, i1); 41 | var c2 = Char.IsDigit(s2, i2); 42 | if (!c1 && !c2) 43 | { 44 | var letter1 = Char.IsLetter(s1, i1); 45 | var letter2 = Char.IsLetter(s2, i2); 46 | if ((letter1 && letter2) || (!letter1 && !letter2)) 47 | { 48 | if (letter1 && letter2) 49 | { 50 | r = Char.ToLower(s1[i1]).CompareTo(Char.ToLower(s2[i2])); 51 | } 52 | else 53 | { 54 | r = s1[i1].CompareTo(s2[i2]); 55 | } 56 | if (r != 0) return r; 57 | } 58 | else if (!letter1 && letter2) return -1; 59 | else if (letter1 && !letter2) return 1; 60 | } 61 | else if (c1 && c2) 62 | { 63 | r = CompareNum(s1, ref i1, s2, ref i2); 64 | if (r != 0) return r; 65 | } 66 | else if (c1) 67 | { 68 | return -1; 69 | } 70 | else if (c2) 71 | { 72 | return 1; 73 | } 74 | i1++; 75 | i2++; 76 | if ((i1 >= s1.Length) && (i2 >= s2.Length)) 77 | { 78 | return 0; 79 | } 80 | else if (i1 >= s1.Length) 81 | { 82 | return -1; 83 | } 84 | else if (i2 >= s2.Length) 85 | { 86 | return 1; 87 | } 88 | } 89 | } 90 | 91 | private Int32 CompareNum(String s1, ref Int32 i1, String s2, ref Int32 i2) 92 | { 93 | Int32 nzStart1 = i1, nzStart2 = i2; // nz = non zero 94 | Int32 end1 = i1, end2 = i2; 95 | 96 | ScanNumEnd(s1, i1, ref end1, ref nzStart1); 97 | ScanNumEnd(s2, i2, ref end2, ref nzStart2); 98 | var start1 = i1; i1 = end1 - 1; 99 | var start2 = i2; i2 = end2 - 1; 100 | 101 | var nzLength1 = end1 - nzStart1; 102 | var nzLength2 = end2 - nzStart2; 103 | 104 | if (nzLength1 < nzLength2) return -1; 105 | else if (nzLength1 > nzLength2) return 1; 106 | 107 | for (Int32 j1 = nzStart1, j2 = nzStart2; j1 <= i1; j1++, j2++) 108 | { 109 | var r = s1[j1].CompareTo(s2[j2]); 110 | if (r != 0) return r; 111 | } 112 | // the nz parts are equal 113 | var length1 = end1 - start1; 114 | var length2 = end2 - start2; 115 | if (length1 == length2) return 0; 116 | if (length1 > length2) return -1; 117 | return 1; 118 | } 119 | 120 | //lookahead 121 | private void ScanNumEnd(String s, Int32 start, ref Int32 end, ref Int32 nzStart) 122 | { 123 | nzStart = start; 124 | end = start; 125 | var countZeros = true; 126 | while (Char.IsDigit(s, end)) 127 | { 128 | if (countZeros && s[end].Equals('0')) 129 | { 130 | nzStart++; 131 | } 132 | else countZeros = false; 133 | end++; 134 | if (end >= s.Length) break; 135 | } 136 | } 137 | }//EOC 138 | } 139 | --------------------------------------------------------------------------------