diff --git a/.gitignore b/.gitignore
index 1f14485237..5b47e32794 100644
--- a/.gitignore
+++ b/.gitignore
@@ -303,4 +303,4 @@ modules/virtual-file-explorer/app/Volo.Abp.VirtualFileExplorer.DemoApp/Logs/
/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/package-lock.json
/templates/app/angular/package-lock.json
/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package-lock.json
-
+abp-build-config.json
diff --git a/docs/en/CLI-BuildCommand.md b/docs/en/CLI-BuildCommand.md
new file mode 100644
index 0000000000..d9620aa621
--- /dev/null
+++ b/docs/en/CLI-BuildCommand.md
@@ -0,0 +1,67 @@
+# Build Command
+
+Building a .NET project is hard when the project references a project reference outside of the solution or even from a different GIT repository. This command builds a GIT repository and it's depending repositories or a single .NET solution File. In order ```build``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain one of;
+
+* A .NET solution file (*.sln)
+* abp-build-config.json
+
+When the executing directory (or ```--working-directory``` parameter's directory) contains a .NET solution file, ```build``` command builds all the projects in the related solution file and all project references on building project files recursively.
+
+When the executing directory (or ```--working-directory``` parameter's directory) contains a ```abp-build-config.json```, ```build``` command builds all changed projects form its last build and all project references on building project files recursively.
+
+# Build Command Config
+
+```abp-build-config.json``` contains properties below;
+
+* ```Name```: Name of the GIT repository. This can be friendly name of your GIT repository or any other unique string for the repository.
+* ```RootPath```: Root path of the repository which contains ```.git``` folder.
+* ```DependingRepositories```: Depending repository list of a repository. Each depending repository item contains same fields as a repository.
+* ```IgnoredDirectories```: Relative directory paths to ignore while building a GIT repository.
+
+A sample ```abp-build-config.json``` looks like for a Windows OS;
+
+````json
+{
+ "Name": "main-repository",
+ "RootPath": "D:\\GitHub\\main-repository",
+ "DependingRepositories": [{
+ "Name": "module-repository",
+ "RootPath": "D:\\GitHub\\module-repository"
+ }],
+ "IgnoredDirectories": [
+ "utils"
+ ]
+}
+````
+
+# Build Status
+
+ABP CLI stores a build status file for builds using the repository friendly names and current branch names in the;
+
+* ```%USERPROFILE%\.abp\build\``` for Windows.
+* ```$HOME/.abp/build/``` for Linux/macOS.
+
+and uses this file when building same repository next time and only builds affected projects and decreases the total build time. A sample build status file content looks like;
+
+````json
+{
+ "RepositoryName": "main-repository",
+ "BranchName": "dev",
+ "CommitId": "84ecde8ba275aeeb14d24a87ad46a1e941adf8ba",
+ "SucceedProjects": [{
+ "CsProjPath": "D:\\GitHub\\main-repository\\BookStore\\BookStore.Web.csproj",
+ "CommitId": "84ecde8ba275aeeb14d24a87ad46a1e941adf8ba"
+ }],
+ "DependingRepositories": [{
+ "RepositoryName": "module-repository",
+ "BranchName": "dev",
+ "CommitId": "0598b8e45af9507fc9ba8abf304e78fc7d434e04",
+ "SucceedProjects": [{
+ "CsProjPath": "D:\\GitHub\\module-repository\\identity-module\Identity\\Identity.Web.csproj",
+ "CommitId": "0598b8e45af9507fc9ba8abf304e78fc7d434e04"
+ }],
+ "DependingRepositories": []
+ }]
+}
+````
+
diff --git a/docs/en/CLI.md b/docs/en/CLI.md
index 8ea20ffd57..dd3287ce12 100644
--- a/docs/en/CLI.md
+++ b/docs/en/CLI.md
@@ -40,6 +40,7 @@ Here, the list of all available commands before explaining their details:
* **`translate`**: Simplifies to translate localization files when you have multiple JSON [localization](Localization.md) files in a source control repository.
* **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password.
* **`logout`**: Logouts from your computer if you've authenticated before.
+* **`build`**: Builds a GIT repository and depending repositories or a single .NET solution.
### help
@@ -360,3 +361,31 @@ Logs you out by removing the session token from your computer.
abp logout
```
+### build
+
+This command builds a GIT repository and it's depending repositories or a single .NET solution File. In order ```build``` command to work, its **executing directory** or passed ```--working-directory``` parameter's directory must contain one of;
+
+* A .NET solution file (*.sln)
+* abp-build-config.json (suggested to add this to .gitignore)
+
+Usage:
+
+````bash
+abp build [options]
+````
+
+Example:
+
+```
+abp build --build-name "prod" --dotnet-build-arguments "\"--no-dependencies\""
+```
+
+#### Options
+
+* ```--working-directory``` or ```-w```: Specifies the working directory. This option is useful when the command is executed outside of a GIT repository or when executing directory doesn't contain a .NET solution file.
+* ```--build-name``` or ```-n```: Specifies a name for the build. This option is useful when same repository is used for more than one different builds.
+* ```--dotnet-build-arguments``` or ```-a```: Arguments to pass ```dotnet build``` when building project files. This parameter must be passed like ```"\"{params}\""``` .
+* ```--force``` or ```-f```: Forces to build projects even they are not changed from the last successful build.
+
+For more details, see [build command documentation](CLI-BuildCommand.md).
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
index 40eecfd06a..2dfd66f4ab 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
+++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
@@ -22,6 +22,7 @@
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
index a79b52b4f2..29238a3e35 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
@@ -36,6 +36,7 @@ namespace Volo.Abp.Cli
options.Commands["switch-to-stable"] = typeof(SwitchToStableCommand);
options.Commands["switch-to-nightly"] = typeof(SwitchToNightlyCommand);
options.Commands["translate"] = typeof(TranslateCommand);
+ options.Commands["build"] = typeof(BuildCommand);
});
}
}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs
new file mode 100644
index 0000000000..6df892b472
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildProjectListSorter.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DefaultBuildProjectListSorter : IBuildProjectListSorter, ITransientDependency
+ {
+ public List SortByDependencies(
+ List source,
+ IEqualityComparer comparer = null)
+ {
+ /* See: http://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp
+ * http://en.wikipedia.org/wiki/Topological_sorting
+ */
+
+ var sorted = new List();
+ var visited = new Dictionary(comparer);
+
+ foreach (var item in source)
+ {
+ SortByDependenciesVisit(source, item, sorted, visited);
+ }
+
+ return sorted;
+ }
+
+ private void SortByDependenciesVisit(
+ List source,
+ DotNetProjectInfo item,
+ List sorted,
+ Dictionary visited)
+ {
+ bool inProcess;
+ var alreadyVisited = visited.TryGetValue(item, out inProcess);
+
+ if (alreadyVisited)
+ {
+ if (inProcess)
+ {
+ throw new ArgumentException("Cyclic dependency found! Item: " + item);
+ }
+ }
+ else
+ {
+ visited[item] = true;
+
+ var dependencies = item.Dependencies;
+ if (dependencies != null)
+ {
+ foreach (var dependency in dependencies)
+ {
+ var dependencyItem = source.FirstOrDefault(e => e.CsProjPath == dependency.CsProjPath);
+ if (dependencyItem != null)
+ {
+ SortByDependenciesVisit(source, dependencyItem, sorted, visited);
+ }
+ }
+ }
+
+ visited[item] = false;
+ sorted.Add(item);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs
new file mode 100644
index 0000000000..e57c6837c0
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultBuildStatusGenerator.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using LibGit2Sharp;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DefaultBuildStatusGenerator : IBuildStatusGenerator, ITransientDependency
+ {
+ private readonly IGitRepositoryHelper _gitRepositoryHelper;
+
+ public DefaultBuildStatusGenerator(IGitRepositoryHelper gitRepositoryHelper)
+ {
+ _gitRepositoryHelper = gitRepositoryHelper;
+ }
+
+ public GitRepositoryBuildStatus Generate(
+ DotNetProjectBuildConfig buildConfig,
+ List changedProjects,
+ List buildSucceededProjects)
+ {
+ var lastCommitId = _gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository);
+ var repoFriendlyName = _gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository);
+
+ var status = new GitRepositoryBuildStatus(
+ buildConfig.GitRepository.Name,
+ repoFriendlyName
+ );
+
+ if (ShouldUpdateRepositoryCommitId(buildConfig, changedProjects, buildSucceededProjects))
+ {
+ status.CommitId = lastCommitId;
+ }
+
+ status.SucceedProjects = changedProjects.Where(p =>
+ p.RepositoryName == buildConfig.GitRepository.Name &&
+ buildSucceededProjects.Contains(p.CsProjPath)
+ )
+ .Select(e => new DotNetProjectBuildStatus
+ {
+ CsProjPath = e.CsProjPath,
+ CommitId = lastCommitId
+ }).ToList();
+
+ foreach (var dependingRepository in buildConfig.GitRepository.DependingRepositories)
+ {
+ GenerateBuildStatusInternal(
+ buildConfig,
+ dependingRepository,
+ changedProjects,
+ buildSucceededProjects,
+ status
+ );
+ }
+
+ return status;
+ }
+
+ private bool ShouldUpdateRepositoryCommitId(
+ DotNetProjectBuildConfig buildConfig,
+ List changedProjects,
+ List buildSucceededProjects)
+ {
+ if (!buildConfig.SlFilePath.IsNullOrEmpty())
+ {
+ return false;
+ }
+
+ if (changedProjects.Count == 0 || buildSucceededProjects.Count == 0)
+ {
+ return false;
+ }
+
+ return changedProjects.Count == buildSucceededProjects.Count;
+ }
+
+ private void GenerateBuildStatusInternal(
+ DotNetProjectBuildConfig buildConfig,
+ GitRepository gitRepository,
+ List changedProjects,
+ List buildSucceededProjects,
+ GitRepositoryBuildStatus status)
+ {
+ var lastCommitId = _gitRepositoryHelper.GetLastCommitId(gitRepository);
+ var repoFriendlyName = _gitRepositoryHelper.GetFriendlyName(gitRepository);
+
+ var dependingRepositoryStatus = new GitRepositoryBuildStatus(
+ gitRepository.Name,
+ repoFriendlyName
+ );
+
+ if (ShouldUpdateRepositoryCommitId(buildConfig, changedProjects, buildSucceededProjects))
+ {
+ dependingRepositoryStatus.CommitId = lastCommitId;
+ }
+
+ dependingRepositoryStatus.SucceedProjects = changedProjects.Where(p =>
+ p.RepositoryName == gitRepository.Name &&
+ buildSucceededProjects.Contains(p.CsProjPath)
+ )
+ .Select(e => new DotNetProjectBuildStatus()
+ {
+ CsProjPath = e.CsProjPath,
+ CommitId = lastCommitId
+ }).ToList();
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ GenerateBuildStatusInternal(
+ buildConfig,
+ dependingRepository,
+ changedProjects,
+ buildSucceededProjects,
+ dependingRepositoryStatus
+ );
+ }
+
+ status.DependingRepositories.Add(dependingRepositoryStatus);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs
new file mode 100644
index 0000000000..e866c425b5
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultChangedProjectFinder.cs
@@ -0,0 +1,352 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using LibGit2Sharp;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DefaultChangedProjectFinder : IChangedProjectFinder, ITransientDependency
+ {
+ private readonly IRepositoryBuildStatusStore _repositoryBuildStatusStore;
+ private readonly IGitRepositoryHelper _gitRepositoryHelper;
+
+ private readonly IDotNetProjectDependencyFiller _dotNetProjectDependencyFiller;
+ private readonly IBuildProjectListSorter _buildProjectListSorter;
+
+ private readonly List _changeDetectionFileExtensions = new List
+ {
+ ".cs",
+ ".csproj",
+ ".cshtml"
+ };
+
+ public DefaultChangedProjectFinder(
+ IRepositoryBuildStatusStore repositoryBuildStatusStore,
+ IGitRepositoryHelper gitRepositoryHelper,
+ IDotNetProjectDependencyFiller dotNetProjectDependencyFiller,
+ IBuildProjectListSorter buildProjectListSorter)
+ {
+ _repositoryBuildStatusStore = repositoryBuildStatusStore;
+ _gitRepositoryHelper = gitRepositoryHelper;
+ _dotNetProjectDependencyFiller = dotNetProjectDependencyFiller;
+ _buildProjectListSorter = buildProjectListSorter;
+ }
+
+ public List Find(DotNetProjectBuildConfig buildConfig)
+ {
+ if (!buildConfig.SlFilePath.IsNullOrEmpty())
+ {
+ return FindBySlnFile(buildConfig);
+ }
+
+ return FindByRepository(buildConfig);
+ }
+
+ ///
+ /// Returns list of projects in a repository and its depending repositories sorted by dependencies
+ ///
+ ///
+ ///
+ private List FindAllProjects(DotNetProjectBuildConfig buildConfig)
+ {
+ var projects = new List();
+
+ AddProjectsOfRepository(buildConfig.GitRepository, projects);
+
+ _dotNetProjectDependencyFiller.Fill(projects);
+
+ var allSortedProjectList = _buildProjectListSorter.SortByDependencies(
+ projects,
+ new DotNetProjectInfoEqualityComparer()
+ );
+
+ FilterIgnoredDirectories(allSortedProjectList, buildConfig.GitRepository);
+
+ return allSortedProjectList;
+ }
+
+ private void FilterIgnoredDirectories(List projects, GitRepository gitRepository)
+ {
+ foreach (var ignoredDirectory in gitRepository.IgnoredDirectories)
+ {
+ projects = projects.Where(e =>
+ !e.CsProjPath.StartsWith(Path.Combine(gitRepository.RootPath, ignoredDirectory)))
+ .ToList();
+ }
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ FilterIgnoredDirectories(projects, dependingRepository);
+ }
+ }
+
+ private void AddProjectsOfRepository(GitRepository gitRepository, List projects)
+ {
+ var allCsProjFiles = Directory.GetFiles(
+ gitRepository.RootPath,
+ "*.csproj",
+ SearchOption.AllDirectories
+ ).ToList();
+
+ projects.AddRange(
+ allCsProjFiles.Select(csProjPath => new DotNetProjectInfo(gitRepository.Name, csProjPath, false))
+ );
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ AddProjectsOfRepository(dependingRepository, projects);
+ }
+ }
+
+ private List FindByRepository(DotNetProjectBuildConfig buildConfig)
+ {
+ var gitRepositoryBuildStatus = _repositoryBuildStatusStore.Get(
+ buildConfig.BuildName,
+ buildConfig.GitRepository
+ );
+
+ var allSortedProjectList = FindAllProjects(buildConfig);
+
+ MarkProjectsForBuild(
+ buildConfig.GitRepository,
+ gitRepositoryBuildStatus,
+ buildConfig.ForceBuild,
+ allSortedProjectList
+ );
+
+ return allSortedProjectList.Where(e => e.ShouldBuild).ToList();
+ }
+
+ private void MarkProjectsForBuild(
+ GitRepository repository,
+ GitRepositoryBuildStatus repositoryBuildStatus,
+ bool forceBuild,
+ List allProjectList)
+ {
+ if (forceBuild || repositoryBuildStatus == null || repositoryBuildStatus.CommitId.IsNullOrEmpty())
+ {
+ // Mark all projects for build
+ allProjectList.ForEach(e => e.ShouldBuild = true);
+ }
+ else
+ {
+ MarkChangedProjectsForBuild(
+ repository,
+ repositoryBuildStatus,
+ allProjectList
+ );
+ }
+
+ if (!repository.DependingRepositories.Any())
+ {
+ return;
+ }
+
+ foreach (var dependingRepository in repository.DependingRepositories)
+ {
+ var dependingRepositoryBuildStatus = repositoryBuildStatus?.GetChild(dependingRepository.Name);
+ MarkProjectsForBuild(
+ dependingRepository,
+ dependingRepositoryBuildStatus,
+ forceBuild,
+ allProjectList
+ );
+ }
+ }
+
+ private void MarkChangedProjectsForBuild(
+ GitRepository repository,
+ GitRepositoryBuildStatus status,
+ List allProjectList)
+ {
+ using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
+ {
+ var firstCommit = status.CommitId.IsNullOrEmpty()
+ ? null
+ : repo.Lookup(status.CommitId);
+
+ var repoDifferences = repo.Diff.Compare(firstCommit?.Tree, repo.Head.Tip.Tree);
+
+ var fileExtensionPredicate = PredicateBuilder.New(true);
+
+ foreach (var changeDetectionFileExtension in _changeDetectionFileExtensions)
+ {
+ fileExtensionPredicate = fileExtensionPredicate.Or(
+ e => e.Path.EndsWith(changeDetectionFileExtension)
+ );
+ }
+
+ var files = repoDifferences
+ .Where(fileExtensionPredicate)
+ .Where(e => e.Status != ChangeKind.Deleted)
+ .Select(e => e)
+ .ToList();
+
+ var affectedCsProjFiles = FindAffectedCsProjFiles(repository.RootPath, files);
+ var lastCommitId = _gitRepositoryHelper.GetLastCommitId(repository);
+
+ foreach (var file in affectedCsProjFiles)
+ {
+ var csProjPath = Path.Combine(repository.RootPath, file);
+ if (status.SucceedProjects.Any(p => p.CsProjPath == csProjPath && p.CommitId == lastCommitId))
+ {
+ continue;
+ }
+
+ // Filter ignored directories
+ var isIgnored = repository.IgnoredDirectories.Any(ignoredDirectory =>
+ csProjPath.StartsWith(Path.Combine(repository.RootPath, ignoredDirectory)));
+ if (isIgnored)
+ {
+ continue;
+ }
+
+ allProjectList.MarkForBuild(repository.Name, csProjPath);
+ AddDependingProjectsToList(repository.Name, csProjPath, allProjectList);
+ }
+ }
+ }
+
+ private List FindBySlnFile(DotNetProjectBuildConfig buildConfig)
+ {
+ var allProjectList = FindAllProjects(buildConfig);
+
+ var slFine = new FileInfo(buildConfig.SlFilePath);
+ var csProjFiles = slFine.Directory.GetFiles(
+ "*.csproj",
+ SearchOption.AllDirectories
+ ).Select(e => e.FullName)
+ .ToList();
+
+ foreach (var csProjFile in csProjFiles)
+ {
+ MarkDependantProjectsForBuild(buildConfig.GitRepository, csProjFile, allProjectList);
+ }
+
+ return _buildProjectListSorter.SortByDependencies(
+ allProjectList,
+ new DotNetProjectInfoEqualityComparer()
+ ).Where(e => e.ShouldBuild).ToList();
+ }
+
+ private void MarkDependantProjectsForBuild(
+ GitRepository gitRepository,
+ string csProjFilePath,
+ List allProjectList)
+ {
+ var repositoryName = gitRepository.FindRepositoryOf(csProjFilePath);
+ var project = new DotNetProjectInfo(repositoryName, csProjFilePath, true);
+
+ if (allProjectList.IsMarkedForBuild(repositoryName, csProjFilePath))
+ {
+ return;
+ }
+
+ allProjectList.MarkForBuild(project);
+ AddProjectDependencies(gitRepository, project, allProjectList);
+ }
+
+ private void AddProjectDependencies(
+ GitRepository gitRepository,
+ DotNetProjectInfo project,
+ List allProjectList)
+ {
+ var projectInfo = allProjectList.FirstOrDefault(e => e.CsProjPath == project.CsProjPath);
+ if (projectInfo == null)
+ {
+ return;
+ }
+
+ var dependencies = projectInfo.Dependencies;
+
+ foreach (var dependency in dependencies)
+ {
+ if (allProjectList.IsMarkedForBuild(dependency.RepositoryName, dependency.CsProjPath))
+ {
+ continue;
+ }
+
+ allProjectList.MarkForBuild(dependency.RepositoryName, dependency.CsProjPath);
+
+ AddProjectDependencies(gitRepository, dependency, allProjectList);
+ }
+ }
+
+ private void AddDependingProjectsToList(
+ string repositoryName,
+ string csProjPath,
+ List allProjectList)
+ {
+ var dependingProjects = allProjectList.Where(
+ e => e.Dependencies.Any(d => d.RepositoryName == repositoryName && d.CsProjPath == csProjPath)
+ ).Select(e => new DotNetProjectInfo(e.RepositoryName, e.CsProjPath, true)).ToList();
+
+ if (!dependingProjects.Any())
+ {
+ return;
+ }
+
+ foreach (var dependingProject in dependingProjects)
+ {
+ if (allProjectList.IsMarkedForBuild(dependingProject))
+ {
+ continue;
+ }
+
+ allProjectList.MarkForBuild(dependingProject);
+ AddDependingProjectsToList(dependingProject.RepositoryName, dependingProject.CsProjPath,
+ allProjectList);
+ }
+ }
+
+ private List FindAffectedCsProjFiles(string repositoryPath, List files)
+ {
+ var affectedProjectFiles = new List();
+ foreach (var file in files)
+ {
+ var filePath = Path.Combine(repositoryPath, file.Path);
+ if (filePath.EndsWith(".csproj"))
+ {
+ affectedProjectFiles.Add(filePath);
+ }
+
+ if (!filePath.EndsWith(".cs"))
+ {
+ continue;
+ }
+
+ var classFile = new FileInfo(filePath);
+ var csProjPath = FindBelongingProjectPathOfClass(classFile.Directory?.FullName);
+ if (csProjPath.IsNullOrEmpty() || affectedProjectFiles.Contains(csProjPath))
+ {
+ continue;
+ }
+
+ affectedProjectFiles.Add(csProjPath);
+ }
+
+ return affectedProjectFiles;
+ }
+
+ private string FindBelongingProjectPathOfClass(string directoryPath)
+ {
+ var files = Directory.GetFiles(directoryPath, "*.csproj", SearchOption.TopDirectoryOnly);
+ if (files.Length == 1)
+ {
+ return files.First();
+ }
+
+ var directoryInfo = new DirectoryInfo(directoryPath);
+
+ if (directoryInfo.Parent == null)
+ {
+ return null;
+ }
+
+ return FindBelongingProjectPathOfClass(directoryInfo.Parent.FullName);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs
new file mode 100644
index 0000000000..65749fd45c
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DefaultDotNetProjectBuilder.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using Volo.Abp.Cli.Utils;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DefaultDotNetProjectBuilder : IDotNetProjectBuilder, ITransientDependency
+ {
+ public List Build(List projects, string arguments)
+ {
+ var builtProjects = new ConcurrentBag();
+ var totalProjectCountToBuild = projects.Count;
+ var buildingProjectIndex = 0;
+
+ try
+ {
+ foreach (var project in projects)
+ {
+ if (builtProjects.Contains(project.CsProjPath))
+ {
+ continue;
+ }
+
+ buildingProjectIndex++;
+
+ Console.WriteLine(
+ "Building....: " + " (" + buildingProjectIndex + "/" +
+ totalProjectCountToBuild + ")" + project.CsProjPath
+ );
+
+ BuildInternal(project, arguments, builtProjects);
+ }
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ }
+
+ return builtProjects.ToList();
+ }
+
+ private void BuildInternal(DotNetProjectInfo project, string arguments, ConcurrentBag builtProjects)
+ {
+ var buildArguments = arguments.TrimStart('"').TrimEnd('"');
+ Console.WriteLine("Executing...: dotnet build " + project.CsProjPath + " " + buildArguments);
+
+ var output = CmdHelper.RunCmdAndGetOutput(
+ "dotnet build " + project.CsProjPath + " " + buildArguments,
+ out int buildStatus
+ );
+
+ if (buildStatus == 0)
+ {
+ builtProjects.Add(project.CsProjPath);
+ WriteOutput(output, ConsoleColor.Green);
+ }
+ else
+ {
+ WriteOutput(output, ConsoleColor.Red);
+ Console.WriteLine("Build failed for :" + project.CsProjPath);
+ throw new Exception("Build failed!");
+ }
+ }
+
+ private void WriteOutput(string text, ConsoleColor color)
+ {
+ var currentConsoleColor = Console.ForegroundColor;
+ Console.ForegroundColor = color;
+ Console.WriteLine(text);
+ Console.ForegroundColor = currentConsoleColor;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs
new file mode 100644
index 0000000000..9a9da0766a
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildConfig.cs
@@ -0,0 +1,13 @@
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectBuildConfig
+ {
+ public string BuildName { get; set; }
+
+ public string SlFilePath { get; set; }
+
+ public GitRepository GitRepository { get; set; }
+
+ public bool ForceBuild { get; set; }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs
new file mode 100644
index 0000000000..10092777c2
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectBuildStatus.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectBuildStatus
+ {
+ public string CsProjPath { get; set; }
+
+ public string CommitId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs
new file mode 100644
index 0000000000..b9c883a45d
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectDependencyFiller.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Microsoft.Extensions.Logging;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectDependencyFiller : IDotNetProjectDependencyFiller, ITransientDependency
+ {
+ public void Fill(List projects)
+ {
+ foreach (var project in projects)
+ {
+ FillProjectDependencies(project);
+ }
+ }
+
+ private void FillProjectDependencies(DotNetProjectInfo project)
+ {
+ var projectNode = XElement.Load(project.CsProjPath);
+ var referenceNodes = projectNode.Descendants("ItemGroup").Descendants("ProjectReference");
+
+ foreach (var referenceNode in referenceNodes)
+ {
+ if (referenceNode.Attribute("Include") == null)
+ {
+ continue;
+ }
+
+ var relativePath = referenceNode.Attribute("Include").Value;
+ var file = new FileInfo(project.CsProjPath);
+ var referenceProjectInfo = new FileInfo(Path.Combine(file.Directory.FullName, relativePath));
+
+ var referenceProject = new DotNetProjectInfo(project.RepositoryName, referenceProjectInfo.FullName, false);
+ project.Dependencies.Add(referenceProject);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs
new file mode 100644
index 0000000000..3a8fe825ca
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfo.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ [Serializable]
+ public class DotNetProjectInfo
+ {
+ public string RepositoryName { get; set; }
+
+ public string CsProjPath { get; set; }
+
+ public bool ShouldBuild { get; set; }
+
+ public List Dependencies { get; set; }
+
+ public DotNetProjectInfo(string repositoryName, string csProjPath, bool shouldBuild)
+ {
+ RepositoryName = repositoryName;
+ CsProjPath = csProjPath;
+ ShouldBuild = shouldBuild;
+ Dependencies = new List();
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs
new file mode 100644
index 0000000000..c6f8e5dd53
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoEqualityComparer.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class DotNetProjectInfoEqualityComparer : EqualityComparer
+ {
+ public override bool Equals(DotNetProjectInfo x, DotNetProjectInfo y)
+ {
+ return (x == null && y == null) || (x != null && y != null && x.CsProjPath == y.CsProjPath);
+ }
+
+ public override int GetHashCode(DotNetProjectInfo obj)
+ {
+ return obj == null ? 0 : obj.CsProjPath.GetHashCode();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs
new file mode 100644
index 0000000000..ace85e5257
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/DotNetProjectInfoExtensions.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Volo.Abp.Cli.Build
+{
+ public static class DotNetProjectInfoExtensions
+ {
+ public static void MarkForBuild(this List projects, string repositoryName, string csProjPath)
+ {
+ var project = projects.FirstOrDefault(e =>
+ e.RepositoryName == repositoryName && e.CsProjPath == csProjPath
+ );
+
+ if (project == null)
+ {
+ return;
+ }
+
+ project.ShouldBuild = true;
+ }
+
+ public static void MarkForBuild(this List projects, DotNetProjectInfo project)
+ {
+ projects.MarkForBuild(project.RepositoryName, project.CsProjPath);
+ }
+
+ public static bool IsMarkedForBuild(this List projects, string repositoryName,
+ string csProjPath)
+ {
+ var project = projects.FirstOrDefault(e =>
+ e.RepositoryName == repositoryName && e.CsProjPath == csProjPath
+ );
+
+ if (project == null)
+ {
+ return false;
+ }
+
+ return project.ShouldBuild;
+ }
+
+ public static bool IsMarkedForBuild(this List projects, DotNetProjectInfo project)
+ {
+ return projects.IsMarkedForBuild(project.RepositoryName, project.CsProjPath);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs
new file mode 100644
index 0000000000..5aed3d0941
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemDotNetProjectBuildConfigReader.cs
@@ -0,0 +1,93 @@
+using System;
+using System.IO;
+using System.Linq;
+using LibGit2Sharp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Json;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class FileSystemDotNetProjectBuildConfigReader : IDotNetProjectBuildConfigReader, ITransientDependency
+ {
+ private readonly IJsonSerializer _jsonSerializer;
+ private string _buildConfigName = "abp-build-config.json";
+
+ public FileSystemDotNetProjectBuildConfigReader(IJsonSerializer jsonSerializer)
+ {
+ _jsonSerializer = jsonSerializer;
+ }
+
+ public DotNetProjectBuildConfig Read(string directoryPath)
+ {
+ var buildConfig = new DotNetProjectBuildConfig();
+ var solutionFiles = Directory.GetFiles(directoryPath, "*.sln", SearchOption.TopDirectoryOnly);
+ if (solutionFiles.Length == 1)
+ {
+ buildConfig.SlFilePath = solutionFiles.First();
+ var configFile = GetClosestFile(directoryPath, _buildConfigName);
+ var configFileContent = File.ReadAllText(configFile);
+ buildConfig.GitRepository = _jsonSerializer.Deserialize(configFileContent);
+
+ SetBranchNames(buildConfig.GitRepository);
+
+ return buildConfig;
+ }
+
+ var configFiles = Directory.GetFiles(directoryPath, _buildConfigName, SearchOption.TopDirectoryOnly);
+ if (configFiles.Length == 1)
+ {
+ var configFile = configFiles.First();
+ var configFileContent = File.ReadAllText(configFile);
+ buildConfig.GitRepository = _jsonSerializer.Deserialize(configFileContent);
+
+ SetBranchNames(buildConfig.GitRepository);
+
+ return buildConfig;
+ }
+
+ Console.WriteLine(
+ "There are more than 1 config (abp-build-config.json) file in the directory!"
+ );
+
+ throw new Exception("There is no solution file (*.sln) or " + _buildConfigName + " in the working directory !");
+ }
+
+ private void SetBranchNames(GitRepository gitRepository)
+ {
+ using (var repo = new Repository(string.Concat(gitRepository.RootPath, @"\.git")))
+ {
+ gitRepository.BranchName = repo.Head.FriendlyName;
+ }
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ SetBranchNames(dependingRepository);
+ }
+ }
+
+ private string GetClosestFile(string directoryPath, string fileName)
+ {
+ var directory = new DirectoryInfo(directoryPath);
+ var files = directory.GetFiles(fileName, SearchOption.TopDirectoryOnly);
+ if (files.Any() && files.Length == 1)
+ {
+ return files.First().FullName;
+ }
+
+ do
+ {
+ directory = directory.Parent;
+ if (directory == null)
+ {
+ return string.Empty;
+ }
+
+ files = directory.GetFiles(fileName, SearchOption.TopDirectoryOnly);
+ if (files.Any() && files.Length == 1)
+ {
+ return files.First().FullName;
+ }
+ } while (true);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs
new file mode 100644
index 0000000000..0f6b2f19a7
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/FileSystemRepositoryBuildStatusStore.cs
@@ -0,0 +1,61 @@
+using System.IO;
+using Newtonsoft.Json;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.IO;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class FileSystemRepositoryBuildStatusStore : IRepositoryBuildStatusStore, ITransientDependency
+ {
+ public GitRepositoryBuildStatus Get(string buildNamePrefix, GitRepository repository)
+ {
+ if (!Directory.Exists(CliPaths.Build))
+ {
+ Directory.CreateDirectory(CliPaths.Build);
+ }
+
+ var buildStatusFile = Path.Combine(CliPaths.Build, repository.GetUniqueName(buildNamePrefix)) +
+ ".json";
+
+ if (!File.Exists(buildStatusFile))
+ {
+ return null;
+ }
+
+ var buildStatusText = File.ReadAllText(buildStatusFile);
+ return JsonConvert.DeserializeObject(buildStatusText);
+ }
+
+ public void Set(string buildNamePrefix, GitRepository repository, GitRepositoryBuildStatus status)
+ {
+ var existingRepositoryStatus = Get(buildNamePrefix, repository);
+
+ var buildStatusFile = Path.Combine(
+ CliPaths.Build,
+ status.GetUniqueName(buildNamePrefix)
+ ) + ".json";
+
+ if (File.Exists(buildStatusFile))
+ {
+ FileHelper.DeleteIfExists(buildStatusFile);
+ }
+
+ if (existingRepositoryStatus != null)
+ {
+ existingRepositoryStatus.MergeWith(status);
+
+ using (var file = File.CreateText(buildStatusFile))
+ {
+ new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, existingRepositoryStatus);
+ }
+ }
+ else
+ {
+ using (var file = File.CreateText(buildStatusFile))
+ {
+ new JsonSerializer {Formatting = Formatting.Indented}.Serialize(file, status);
+ }
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs
new file mode 100644
index 0000000000..eb1acafd82
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepository.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ ///
+ /// Represents a source code repository
+ ///
+ public class GitRepository
+ {
+ ///
+ /// Name of the repository
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Branch of the repository
+ ///
+ public string BranchName { get; set; }
+
+ ///
+ /// Root path of the repository which contains .git folder
+ ///
+ public string RootPath { get; set; }
+
+ public List DependingRepositories { get; set; }
+
+ public List IgnoredDirectories { get; set; }
+
+ public GitRepository(string name, string branchName, string rootPath)
+ {
+ Name = name;
+ BranchName = branchName;
+ RootPath = rootPath;
+ DependingRepositories = new List();
+ IgnoredDirectories = new List();
+ }
+
+ public string GetUniqueName(string prefix)
+ {
+ var name = Name + "_" + BranchName;
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+
+ return (prefix.IsNullOrEmpty() ? "" : prefix + "_") + name.ToMd5();
+ }
+
+ private void AddToUniqueName(GitRepository gitRepository, string name)
+ {
+ name += "_" + gitRepository.Name + "_" + gitRepository.BranchName;
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+ }
+
+ public string FindRepositoryOf(string csProjFilePath)
+ {
+ if (csProjFilePath.StartsWith(RootPath))
+ {
+ return Name;
+ }
+
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ var name = FindRepositoryOfInternal(dependingRepository, csProjFilePath);
+ if (!string.IsNullOrEmpty(name))
+ {
+ return name;
+ }
+ }
+
+ return null;
+ }
+
+ private string FindRepositoryOfInternal(GitRepository repository, string csProjFilePath)
+ {
+ if (csProjFilePath.StartsWith(repository.RootPath))
+ {
+ return repository.Name;
+ }
+
+ foreach (var dependingRepository in repository.DependingRepositories)
+ {
+ return FindRepositoryOfInternal(dependingRepository, csProjFilePath);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs
new file mode 100644
index 0000000000..1a60a04068
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryBuildStatus.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class GitRepositoryBuildStatus
+ {
+ ///
+ /// Name of the repository
+ ///
+ public string RepositoryName { get; set; }
+
+ ///
+ /// Branch of the repository
+ ///
+ public string BranchName { get; set; }
+
+ ///
+ /// Last succeeded commitId of the repository
+ ///
+ public string CommitId { get; set; }
+
+ ///
+ /// Build succeeded projects of this repository
+ ///
+ public List SucceedProjects { get; set; }
+
+ ///
+ /// Build status of depending repositories
+ ///
+ public List DependingRepositories { get; set; }
+
+ public GitRepositoryBuildStatus(string repositoryName, string branchName)
+ {
+ RepositoryName = repositoryName;
+ BranchName = branchName;
+ SucceedProjects = new List();
+ DependingRepositories = new List();
+ }
+
+ public GitRepositoryBuildStatus GetSelfOrChild(string repositoryName)
+ {
+ if (RepositoryName == repositoryName)
+ {
+ return this;
+ }
+
+ return GetChild(repositoryName);
+ }
+
+ public GitRepositoryBuildStatus GetChild(string repositoryName)
+ {
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ var child = GetChildInternal(dependingRepository, repositoryName);
+ if (child != null)
+ {
+ return child;
+ }
+ }
+
+ return null;
+ }
+
+ public string GetUniqueName(string prefix)
+ {
+ var name = RepositoryName + "_" + BranchName;
+ foreach (var dependingRepository in DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+
+ return (prefix.IsNullOrEmpty() ? "" : prefix + "_") + name.ToMd5();
+ }
+
+ public void AddOrUpdateProjectStatus(DotNetProjectBuildStatus status)
+ {
+ var existingProjectStatus = SucceedProjects.FirstOrDefault(p => p.CsProjPath == status.CsProjPath);
+ if (existingProjectStatus != null)
+ {
+ existingProjectStatus.CommitId = status.CommitId;
+ }
+ else
+ {
+ SucceedProjects.Add(status);
+ }
+ }
+
+ public void MergeWith(GitRepositoryBuildStatus newBuildStatus)
+ {
+ if (!newBuildStatus.CommitId.IsNullOrEmpty())
+ {
+ CommitId = newBuildStatus.CommitId;
+ }
+
+ foreach (var succeedProject in newBuildStatus.SucceedProjects)
+ {
+ AddOrUpdateProjectStatus(succeedProject);
+ }
+
+ foreach (var dependingRepositoryBuildStatus in newBuildStatus.DependingRepositories)
+ {
+ var existingDependingRepositoryBuildStatus = GetChild(dependingRepositoryBuildStatus.RepositoryName);
+ var newDependingRepositoryBuildStatus = newBuildStatus.GetChild(
+ dependingRepositoryBuildStatus.RepositoryName
+ );
+
+ existingDependingRepositoryBuildStatus.MergeWith(newDependingRepositoryBuildStatus);
+ }
+ }
+
+ private GitRepositoryBuildStatus GetChildInternal(GitRepositoryBuildStatus repositoryBuildStatus,
+ string repositoryName)
+ {
+ if (repositoryBuildStatus.RepositoryName == repositoryName)
+ {
+ return repositoryBuildStatus;
+ }
+
+ foreach (var dependingRepository in repositoryBuildStatus.DependingRepositories)
+ {
+ var child = GetChildInternal(dependingRepository, repositoryName);
+ if (child != null)
+ {
+ return child;
+ }
+ }
+
+ return null;
+ }
+
+ private void AddToUniqueName(GitRepositoryBuildStatus gitRepository, string name)
+ {
+ name += "_" + gitRepository.RepositoryName + "_" + gitRepository.BranchName;
+
+ foreach (var dependingRepository in gitRepository.DependingRepositories)
+ {
+ AddToUniqueName(dependingRepository, name);
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs
new file mode 100644
index 0000000000..51c84f0f61
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/GitRepositoryHelper.cs
@@ -0,0 +1,24 @@
+using LibGit2Sharp;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class GitRepositoryHelper : IGitRepositoryHelper, ITransientDependency
+ {
+ public string GetLastCommitId(GitRepository repository)
+ {
+ using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
+ {
+ return repo.Head.Tip.Id.ToString();
+ }
+ }
+
+ public string GetFriendlyName(GitRepository repository)
+ {
+ using (var repo = new Repository(string.Concat(repository.RootPath, @"\.git")))
+ {
+ return repo.Head.FriendlyName;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs
new file mode 100644
index 0000000000..ed97ca27c8
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildProjectListSorter.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IBuildProjectListSorter
+ {
+ List SortByDependencies(
+ List source,
+ IEqualityComparer comparer = null);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs
new file mode 100644
index 0000000000..f9cf0b8254
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IBuildStatusGenerator.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IBuildStatusGenerator
+ {
+ GitRepositoryBuildStatus Generate(
+ DotNetProjectBuildConfig buildConfig,
+ List changedProjects,
+ List buildSucceededProjects
+ );
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs
new file mode 100644
index 0000000000..b381a073b4
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IChangedProjectFinder.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IChangedProjectFinder
+ {
+ List Find(DotNetProjectBuildConfig buildConfig);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs
new file mode 100644
index 0000000000..54dd2bd5ac
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuildConfigReader.cs
@@ -0,0 +1,7 @@
+namespace Volo.Abp.Cli.Build
+{
+ public interface IDotNetProjectBuildConfigReader
+ {
+ DotNetProjectBuildConfig Read(string directoryPath);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs
new file mode 100644
index 0000000000..02f753c325
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectBuilder.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IDotNetProjectBuilder
+ {
+ List Build(List projects, string arguments);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs
new file mode 100644
index 0000000000..cc1ba65bc7
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IDotNetProjectDependencyFiller.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.Cli.Build
+{
+ public interface IDotNetProjectDependencyFiller
+ {
+ void Fill(List projects);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs
new file mode 100644
index 0000000000..a1402c7dda
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IGitRepositoryHelper.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Cli.Build
+{
+ public interface IGitRepositoryHelper
+ {
+ string GetLastCommitId(GitRepository repository);
+
+ string GetFriendlyName(GitRepository repository);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs
new file mode 100644
index 0000000000..99be400ec8
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Build/IRepositoryBuildStatusStore.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.Cli.Build
+{
+ public interface IRepositoryBuildStatusStore
+ {
+ GitRepositoryBuildStatus Get(string buildNamePrefix, GitRepository repository);
+
+ void Set(string buildNamePrefix, GitRepository repository, GitRepositoryBuildStatus status);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
index eb6339b9da..2b422446ca 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/CliPaths.cs
@@ -9,7 +9,8 @@ namespace Volo.Abp.Cli
public static string Log => Path.Combine(AbpRootPath, "cli", "logs");
public static string Root => Path.Combine(AbpRootPath, "cli");
public static string AccessToken => Path.Combine(AbpRootPath, "cli", "access-token.bin");
-
+ public static string Build => Path.Combine(AbpRootPath, "build");
+
private static readonly string AbpRootPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".abp");
}
}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs
new file mode 100644
index 0000000000..6d28b39fad
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/BuildCommand.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Volo.Abp.Cli.Args;
+using Volo.Abp.Cli.Build;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Commands
+{
+ public class BuildCommand : IConsoleCommand, ITransientDependency
+ {
+ public IDotNetProjectDependencyFiller DotNetProjectDependencyFiller { get; set; }
+
+ public IChangedProjectFinder ChangedProjectFinder { get; set; }
+
+ public IDotNetProjectBuilder DotNetProjectBuilder { get; set; }
+
+ public IRepositoryBuildStatusStore RepositoryBuildStatusStore { get; set; }
+
+ public IDotNetProjectBuildConfigReader DotNetProjectBuildConfigReader { get; set; }
+
+ public IBuildStatusGenerator BuildStatusGenerator { get; set; }
+
+ public IBuildProjectListSorter BuildProjectListSorter { get; set; }
+
+ public Task ExecuteAsync(CommandLineArgs commandLineArgs)
+ {
+ var sw = new Stopwatch();
+ sw.Start();
+
+ var workingDirectory = commandLineArgs.Options.GetOrNull(
+ Options.WorkingDirectory.Short,
+ Options.WorkingDirectory.Long
+ );
+
+ var dotnetBuildArguments = commandLineArgs.Options.GetOrNull(
+ Options.DotnetBuildArguments.Short,
+ Options.DotnetBuildArguments.Long
+ );
+
+ var buildName = commandLineArgs.Options.GetOrNull(
+ Options.BuildName.Short,
+ Options.BuildName.Long
+ );
+
+ var forceBuild = commandLineArgs.Options.ContainsKey(Options.ForceBuild.Short) ||
+ commandLineArgs.Options.ContainsKey(Options.ForceBuild.Long);
+
+ var buildConfig = DotNetProjectBuildConfigReader.Read(workingDirectory ?? Directory.GetCurrentDirectory());
+ buildConfig.BuildName = buildName;
+ buildConfig.ForceBuild = forceBuild;
+
+ Console.WriteLine("Finding changed projects...");
+
+ var changedProjectFiles = ChangedProjectFinder.Find(buildConfig);
+
+ var buildSucceededProjects = DotNetProjectBuilder.Build(
+ changedProjectFiles,
+ dotnetBuildArguments ?? ""
+ );
+
+ var buildStatus = BuildStatusGenerator.Generate(
+ buildConfig,
+ changedProjectFiles,
+ buildSucceededProjects
+ );
+
+ RepositoryBuildStatusStore.Set(buildName, buildConfig.GitRepository, buildStatus);
+
+ sw.Stop();
+ Console.WriteLine("Build operation is completed in " + sw.ElapsedMilliseconds + " (ms)");
+
+ return Task.CompletedTask;
+ }
+
+ public string GetUsageInfo()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("");
+ sb.AppendLine("Usage:");
+ sb.AppendLine("");
+ sb.AppendLine(" abp build [options]");
+ sb.AppendLine("");
+ sb.AppendLine("Options:");
+ sb.AppendLine("");
+ sb.AppendLine("-wd|--working-directory (default: empty)");
+ sb.AppendLine("-m |--max-parallel-builds (default: 1)");
+ sb.AppendLine("-a |--dotnet-build-arguments (default: empty)");
+ sb.AppendLine("-n |--build-name (default: empty)");
+ sb.AppendLine("-f | --force (default: false)");
+ sb.AppendLine("");
+ sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");
+
+ return sb.ToString();
+ }
+
+ public string GetShortDescription()
+ {
+ return "Builds a dotnet repository and dependent repositories or a solution.";
+ }
+
+ public static class Options
+ {
+ public static class WorkingDirectory
+ {
+ public const string Short = "wd";
+ public const string Long = "working-directory";
+ }
+
+ public static class DotnetBuildArguments
+ {
+ public const string Short = "a";
+ public const string Long = "dotnet-build-arguments";
+ }
+
+ public static class BuildName
+ {
+ public const string Short = "n";
+ public const string Long = "build-name";
+ }
+
+ public static class ForceBuild
+ {
+ public const string Short = "f";
+ public const string Long = "force";
+ }
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs
index ac71649793..23fed34f3c 100644
--- a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs
+++ b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs
@@ -188,18 +188,22 @@ namespace System.Collections.Generic
/// The type of the members of values.
/// A list of objects to sort
/// Function to resolve the dependencies
+ /// Equality comparer for dependencies
///
/// Returns a new list ordered by dependencies.
/// If A depends on B, then B will come before than A in the resulting list.
///
- public static List SortByDependencies(this IEnumerable source, Func> getDependencies)
+ public static List SortByDependencies(
+ this IEnumerable source,
+ Func> getDependencies,
+ IEqualityComparer comparer = null)
{
/* See: http://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp
* http://en.wikipedia.org/wiki/Topological_sorting
*/
var sorted = new List();
- var visited = new Dictionary();
+ var visited = new Dictionary(comparer);
foreach (var item in source)
{
diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildProjectListSorter_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildProjectListSorter_Tests.cs
new file mode 100644
index 0000000000..1fde954ce2
--- /dev/null
+++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildProjectListSorter_Tests.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class BuildProjectListSorter_Tests : AbpCliTestBase
+ {
+ private IBuildProjectListSorter _buildProjectListSorter;
+
+ public BuildProjectListSorter_Tests()
+ {
+ _buildProjectListSorter = GetRequiredService();
+ }
+
+ [Fact]
+ public void SortByDependencies_Test()
+ {
+ // A -> B, C
+ // B -> D
+ // D -> F
+ // F -> C
+ // C -> G
+ // Final build order must be: G,
+
+ var repositoryName = "volo";
+ var source = new List
+ {
+ new DotNetProjectInfo(repositoryName, "A", true)
+ {
+ Dependencies = new List()
+ {
+ new DotNetProjectInfo(repositoryName, "B", true),
+ new DotNetProjectInfo(repositoryName, "C", true)
+ }
+ },
+ new DotNetProjectInfo(repositoryName, "B", true)
+ {
+ Dependencies = new List()
+ {
+ new DotNetProjectInfo(repositoryName, "D", true)
+ }
+ },
+ new DotNetProjectInfo(repositoryName, "D", true)
+ {
+ Dependencies = new List()
+ {
+ new DotNetProjectInfo(repositoryName, "F", true)
+ }
+ },
+ new DotNetProjectInfo(repositoryName, "F", true)
+ {
+ Dependencies = new List()
+ {
+ new DotNetProjectInfo(repositoryName, "C", true)
+ }
+ },
+ new DotNetProjectInfo(repositoryName, "C", true)
+ {
+ Dependencies = new List()
+ {
+ new DotNetProjectInfo(repositoryName, "G", true)
+ }
+ },
+ new DotNetProjectInfo(repositoryName, "G", true)
+ };
+
+ var sortedDependencies =
+ _buildProjectListSorter.SortByDependencies(source, new DotNetProjectInfoEqualityComparer());
+ sortedDependencies.Count.ShouldBe(6);
+ sortedDependencies[0].CsProjPath.ShouldBe("G");
+ sortedDependencies[1].CsProjPath.ShouldBe("C");
+ sortedDependencies[2].CsProjPath.ShouldBe("F");
+ sortedDependencies[3].CsProjPath.ShouldBe("D");
+ sortedDependencies[4].CsProjPath.ShouldBe("B");
+ sortedDependencies[5].CsProjPath.ShouldBe("A");
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildStatusGenerator_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildStatusGenerator_Tests.cs
new file mode 100644
index 0000000000..24790a8a03
--- /dev/null
+++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/BuildStatusGenerator_Tests.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class BuildStatusGenerator_Tests : AbpCliTestBase
+ {
+ private readonly IBuildStatusGenerator _buildStatusGenerator;
+ private IGitRepositoryHelper _gitRepositoryHelper;
+
+ public BuildStatusGenerator_Tests()
+ {
+ _buildStatusGenerator = GetRequiredService();
+ }
+
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ _gitRepositoryHelper = Substitute.For();
+ services.AddTransient(provider => _gitRepositoryHelper);
+ }
+
+ [Fact]
+ public void Should_Set_Repository_CommitId_When_All_Projects_Built()
+ {
+ var buildConfig = new DotNetProjectBuildConfig
+ {
+ GitRepository = new GitRepository("volo", "dev", "")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepository("abp", "dev", "")
+ }
+ }
+ };
+
+ var changedProjects = new List()
+ {
+ new DotNetProjectInfo("volo", "project1.csproj", true)
+ };
+
+ var builtProjects = new List()
+ {
+ "project1.csproj"
+ };
+
+ var lastCommitId = "1";
+ _gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository).Returns(lastCommitId);
+ _gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository).Returns("volo");
+
+ var status = _buildStatusGenerator.Generate(buildConfig, changedProjects, builtProjects);
+ status.CommitId.ShouldBe(lastCommitId);
+ }
+
+
+ [Fact]
+ public void Should_Set_Repository_CommitId_When_All_Projects_Built_For_Child_Repository()
+ {
+ var buildConfig = new DotNetProjectBuildConfig
+ {
+ GitRepository = new GitRepository("volo", "dev", "")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepository("abp", "dev", "")
+ }
+ }
+ };
+
+ var changedProjects = new List()
+ {
+ new DotNetProjectInfo("abp", "project1.csproj", true)
+ };
+
+ var builtProjects = new List()
+ {
+ "project1.csproj"
+ };
+
+ var lastCommitId = "1";
+ _gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository).Returns(lastCommitId);
+ _gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository).Returns("abp");
+
+ var status = _buildStatusGenerator.Generate(buildConfig, changedProjects, builtProjects);
+ status.CommitId.ShouldBe(lastCommitId);
+ }
+
+ [Fact]
+ public void Should_Not_Set_Repository_CommitId_When_Building_Single_Solution()
+ {
+ var buildConfig = new DotNetProjectBuildConfig
+ {
+ GitRepository = new GitRepository("volo", "dev", "")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepository("abp", "dev", "")
+ }
+ },
+ SlFilePath = "test.sln"
+ };
+
+ var changedProjects = new List()
+ {
+ new DotNetProjectInfo("volo", "project1.csproj", true)
+ };
+
+ var builtProjects = new List()
+ {
+ "project1.csproj"
+ };
+
+ var lastCommitId = "1";
+ _gitRepositoryHelper.GetLastCommitId(buildConfig.GitRepository).Returns(lastCommitId);
+ _gitRepositoryHelper.GetFriendlyName(buildConfig.GitRepository).Returns("volo");
+
+ var status = _buildStatusGenerator.Generate(buildConfig, changedProjects, builtProjects);
+ status.CommitId.ShouldBeNull();
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepositoryBuildStatus_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepositoryBuildStatus_Tests.cs
new file mode 100644
index 0000000000..387b30da7c
--- /dev/null
+++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepositoryBuildStatus_Tests.cs
@@ -0,0 +1,318 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class GitRepositoryBuildStatus_Tests : AbpCliTestBase
+ {
+ private IGitRepositoryHelper _gitRepositoryHelper;
+
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ _gitRepositoryHelper = Substitute.For();
+ services.AddTransient(provider => _gitRepositoryHelper);
+ }
+
+ [Fact]
+ public void Add_New_Build_Status_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project1.csproj",
+ CommitId = "1"
+ }
+ }
+ };
+
+ var newBuildStatus = new GitRepositoryBuildStatus(
+ existingBuildStatus.RepositoryName,
+ existingBuildStatus.BranchName
+ )
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project2.csproj",
+ CommitId = "2"
+ }
+ }
+ };
+
+ existingBuildStatus.MergeWith(newBuildStatus);
+
+ existingBuildStatus.SucceedProjects.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public void Update_Existing_Build_Status_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project1.csproj",
+ CommitId = "1"
+ }
+ }
+ };
+
+ var newBuildStatus = new GitRepositoryBuildStatus(
+ existingBuildStatus.RepositoryName,
+ existingBuildStatus.BranchName
+ )
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project1.csproj",
+ CommitId = "2"
+ },
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project2.csproj",
+ CommitId = "2"
+ }
+ }
+ };
+
+ existingBuildStatus.MergeWith(newBuildStatus);
+ existingBuildStatus.SucceedProjects.Count.ShouldBe(2);
+ existingBuildStatus.GetSelfOrChild("volo").SucceedProjects.First(p => p.CsProjPath == "project1.csproj")
+ .CommitId.ShouldBe("2");
+ existingBuildStatus.GetSelfOrChild("volo").SucceedProjects.First(p => p.CsProjPath == "project2.csproj")
+ .CommitId.ShouldBe("2");
+ }
+
+ [Fact]
+ public void Add_New_Build_Status_For_Child_Repository_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("abp", "dev")
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project1.csproj",
+ CommitId = "1"
+ }
+ }
+ }
+ }
+ };
+
+ var newBuildStatus = new GitRepositoryBuildStatus(
+ existingBuildStatus.RepositoryName,
+ existingBuildStatus.BranchName
+ )
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("abp", "dev")
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "project2.csproj",
+ CommitId = "2"
+ }
+ }
+ }
+ }
+ };
+
+ existingBuildStatus.MergeWith(newBuildStatus);
+ existingBuildStatus.GetChild("abp").SucceedProjects.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public void Should_Update_Repository_CommitId_When_New_CommitId_Is_Not_Empty()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev");
+
+ var newBuildStatus = new GitRepositoryBuildStatus(
+ existingBuildStatus.RepositoryName,
+ existingBuildStatus.BranchName
+ )
+ {
+ CommitId = "42"
+ };
+
+ existingBuildStatus.MergeWith(newBuildStatus);
+
+ existingBuildStatus.CommitId.ShouldBe("42");
+ }
+
+ [Fact]
+ public void Should_Not_Update_Repository_CommitId_When_New_CommitId_Is_Empty()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("volo", "dev")
+ {
+ CommitId = "21"
+ };
+
+ var newBuildStatus = new GitRepositoryBuildStatus(
+ existingBuildStatus.RepositoryName,
+ existingBuildStatus.BranchName
+ )
+ {
+ CommitId = ""
+ };
+
+ existingBuildStatus.MergeWith(newBuildStatus);
+
+ existingBuildStatus.CommitId.ShouldBe("21");
+ }
+
+ [Fact]
+ public void GetChild_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-2", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-3", "dev")
+ }
+ },
+ new GitRepositoryBuildStatus("repo-4", "dev")
+ }
+ };
+
+ existingBuildStatus.GetChild("repo-3").RepositoryName.ShouldBe("repo-3");
+ existingBuildStatus.GetChild("repo-4").RepositoryName.ShouldBe("repo-4");
+ }
+
+ [Fact]
+ public void GetUniqueName_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-2", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-3", "dev")
+ }
+ },
+ new GitRepositoryBuildStatus("repo-4", "dev")
+ }
+ };
+
+ existingBuildStatus.GetUniqueName("").ShouldBe("B25C935F97D7B3375530A96B392B7644");
+ existingBuildStatus.GetUniqueName("production").ShouldBe("production_B25C935F97D7B3375530A96B392B7644");
+ }
+
+ [Fact]
+ public void GetSelfOrChild_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-2", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-3", "dev")
+ }
+ },
+ new GitRepositoryBuildStatus("repo-4", "dev")
+ }
+ };
+
+ existingBuildStatus.GetSelfOrChild("repo-1").RepositoryName.ShouldBe("repo-1");
+ existingBuildStatus.GetSelfOrChild("repo-2").RepositoryName.ShouldBe("repo-2");
+ existingBuildStatus.GetSelfOrChild("repo-3").RepositoryName.ShouldBe("repo-3");
+ existingBuildStatus.GetSelfOrChild("repo-4").RepositoryName.ShouldBe("repo-4");
+ }
+
+ [Fact]
+ public void AddOrUpdateProjectStatus_Test()
+ {
+ var existingBuildStatus = new GitRepositoryBuildStatus("repo-1", "dev")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepositoryBuildStatus("repo-2", "dev")
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "A.csproj",
+ CommitId = "42"
+ }
+ },
+ DependingRepositories = new List
+ {
+ new GitRepositoryBuildStatus("repo-3", "dev")
+ {
+ SucceedProjects = new List
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "B.csproj",
+ CommitId = "42"
+ }
+ }
+ }
+ }
+ },
+ new GitRepositoryBuildStatus("repo-4", "dev")
+ {
+ SucceedProjects = new List()
+ {
+ new DotNetProjectBuildStatus
+ {
+ CsProjPath = "C.csproj",
+ CommitId = "42"
+ }
+ }
+ }
+ }
+ };
+
+ var repo2 = existingBuildStatus.GetChild("repo-2");
+ repo2.AddOrUpdateProjectStatus(new DotNetProjectBuildStatus
+ {
+ CommitId = "21",
+ CsProjPath = "A.csproj"
+ });
+
+ var repo3 = existingBuildStatus.GetChild("repo-3");
+ repo3.AddOrUpdateProjectStatus(new DotNetProjectBuildStatus
+ {
+ CommitId = "21",
+ CsProjPath = "X.csproj"
+ });
+
+ repo2.SucceedProjects.Count.ShouldBe(1);
+ repo2.SucceedProjects.ShouldContain(e=> e.CsProjPath == "A.csproj" && e.CommitId == "21");
+
+ repo3.SucceedProjects.Count.ShouldBe(2);
+ repo3.SucceedProjects.ShouldContain(e=> e.CsProjPath == "X.csproj" && e.CommitId == "21");
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepository_Tests.cs b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepository_Tests.cs
new file mode 100644
index 0000000000..17cc8b9dc4
--- /dev/null
+++ b/framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/Build/GitRepository_Tests.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.Cli.Build
+{
+ public class GitRepository_Tests : AbpCliTestBase
+ {
+ [Fact]
+ public void GetUniqueName_Test()
+ {
+ var gitRepository = new GitRepository("repo-1", "dev", "")
+ {
+ DependingRepositories = new List
+ {
+ new GitRepository("repo-2", "dev", ""),
+ new GitRepository("repo-3", "dev", "")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepository("repo-4", "dev", "")
+ }
+ }
+ }
+ };
+
+ gitRepository.GetUniqueName("").ShouldBe("B25C935F97D7B3375530A96B392B7644");
+ gitRepository.GetUniqueName("production").ShouldBe("production_B25C935F97D7B3375530A96B392B7644");
+ }
+
+ [Fact]
+ public void FindRepositoryOf_Test()
+ {
+ var gitRepository = new GitRepository("repo-1", "dev", "/repo-1/dev/")
+ {
+ DependingRepositories = new List
+ {
+ new GitRepository("repo-2", "dev", "/repo-2/dev/"),
+ new GitRepository("repo-3", "dev", "/repo-3/dev/")
+ {
+ DependingRepositories = new List()
+ {
+ new GitRepository("repo-4", "dev", "/repo-4/dev/")
+ }
+ }
+ }
+ };
+
+ gitRepository.FindRepositoryOf("/repo-4/dev/A.csproj").ShouldBe("repo-4");
+ }
+ }
+}