├── stack-ad ├── .gitignore ├── readme.md └── ad.py ├── src ├── ScmBackup.Tests.Integration │ ├── brokensettings.yml │ ├── Scm │ │ ├── FakeCommandLineScmTools │ │ │ ├── FakeCommandLineScm-Command-Linux.sh │ │ │ └── FakeCommandLineScm-Command-Windows.bat │ │ ├── CommandLineScmTests.cs │ │ ├── MercurialScmTests.cs │ │ └── GitScmTests.cs │ ├── xunit.runner.json │ ├── settings-unknown-property.yml │ ├── NLog.config │ ├── testsettings.yml │ ├── Hosters │ │ ├── HttpLogHelper.cs │ │ ├── GitlabApiTests.cs │ │ ├── BitbucketApiTests.cs │ │ ├── GitlabBackupTests.cs │ │ └── BitbucketBackupGitTests.cs │ ├── ScmFactoryTests.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── TestLogger.cs │ ├── HosterFactoryTests.cs │ ├── DirectoryHelperTests.cs │ ├── DirectoryHelper.cs │ ├── ConfigBackupMakerTests.cs │ ├── BootstrapperTests.cs │ ├── ConfigReaderTests.cs │ └── ScmBackup.Tests.Integration.csproj ├── ScmBackup │ ├── IScmBackup.cs │ ├── Http │ │ ├── IEmailSender.cs │ │ ├── IUrlHelper.cs │ │ ├── HttpResult.cs │ │ ├── IHttpRequest.cs │ │ ├── UrlHelper.cs │ │ ├── HttpRequest.cs │ │ ├── LoggingHttpRequest.cs │ │ └── MailKitEmailSender.cs │ ├── AssemblyCopyright.tt │ ├── Scm │ │ ├── ScmAttribute.cs │ │ ├── IScmFactory.cs │ │ ├── IScmValidator.cs │ │ ├── ScmCredentials.cs │ │ ├── CommandLineResult.cs │ │ ├── ScmValidator.cs │ │ └── IScm.cs │ ├── ScmType.cs │ ├── IHosterValidator.cs │ ├── CompositionRoot │ │ ├── IHosterFactory.cs │ │ ├── HosterValidator.cs │ │ ├── HosterApiCaller.cs │ │ ├── HosterBackupMaker.cs │ │ ├── ScmFactory.cs │ │ └── HosterFactory.cs │ ├── IApiCaller.cs │ ├── Hosters │ │ ├── Gitlab │ │ │ ├── GitlabApiWiki.cs │ │ │ ├── GitlabHoster.cs │ │ │ ├── GitlabApiRepo.cs │ │ │ ├── GitlabConfigSourceValidator.cs │ │ │ └── GitlabBackup.cs │ │ ├── IHoster.cs │ │ ├── IBackup.cs │ │ ├── IConfigSourceValidator.cs │ │ ├── IHosterApi.cs │ │ ├── Github │ │ │ ├── GithubConfigSourceValidator.cs │ │ │ ├── GithubHoster.cs │ │ │ └── GithubBackup.cs │ │ ├── Bitbucket │ │ │ ├── BitbucketConfigSourceValidator.cs │ │ │ ├── BitbucketHoster.cs │ │ │ ├── BitbucketBackup.cs │ │ │ └── BitbucketApiResponse.cs │ │ ├── HosterNameHelper.cs │ │ ├── HosterRepository.cs │ │ └── ConfigSourceValidatorBase.cs │ ├── IHosterApiCaller.cs │ ├── Configuration │ │ ├── IConfigReader.cs │ │ ├── ConfigScm.cs │ │ ├── IConfigBackupMaker.cs │ │ ├── ConfigOptions.cs │ │ ├── ConfigEmail.cs │ │ ├── Config.cs │ │ ├── ConfigReader.cs │ │ ├── EnvironmentVariableConfigReader.cs │ │ ├── AddTimestampedSubfolderConfigReader .cs │ │ ├── ConfigBackupMaker.cs │ │ ├── ValidationResult.cs │ │ └── ConfigSource.cs │ ├── IBackupMaker.cs │ ├── Properties │ │ ├── launchSettings.json │ │ └── AssemblyInfo.cs │ ├── IDeletedRepoHandler.cs │ ├── Program.cs │ ├── IHosterBackupMaker.cs │ ├── ErrorLevel.cs │ ├── IContext.cs │ ├── ValidationMessageType.cs │ ├── NLog.config │ ├── ILogger.cs │ ├── LoggingHosterApiCaller.cs │ ├── IgnoringHosterApiCaller.cs │ ├── IncludingHosterApiCaller.cs │ ├── LoggingScmBackup.cs │ ├── ApiCaller.cs │ ├── IFileSystemHelper.cs │ ├── Dockerfile │ ├── Loggers │ │ ├── ConsoleLogger.cs │ │ ├── NLogLogger.cs │ │ ├── CompositeLogger.cs │ │ └── EmailLogger.cs │ ├── Context.cs │ ├── settings.yml │ ├── ApiRepositories.cs │ ├── ScmBackup.cs │ ├── DeletedRepoHandler.cs │ ├── ErrorHandlingScmBackup.cs │ ├── ScmBackup.csproj │ └── FileSystemHelper.cs └── ScmBackup.Tests │ ├── Hosters │ ├── FooBarBar.cs │ ├── FakeConfigSourceValidator.cs │ ├── FakeHosterApi.cs │ ├── CloneUrlBuilderTests.cs │ ├── GithubConfigSourceValidatorTests.cs │ ├── BitbucketConfigSourceValidatorTests.cs │ ├── BackupBaseTests.cs │ ├── GitlabConfigSourceValidatorTests.cs │ ├── CloneUrlBuilder.cs │ ├── FakeHosterFactory.cs │ ├── FakeHosterApiCaller.cs │ ├── FakeHosterBackup.cs │ ├── HosterValidatorTests.cs │ ├── HosterApiCallerTests.cs │ ├── HosterNameHelperTests.cs │ ├── HosterBackupMakerTests.cs │ ├── FakeHoster.cs │ └── HosterRepositoryTests.cs │ ├── Configuration │ ├── ConfigTests.cs │ ├── ConfigEmailTests.cs │ └── EnvironmentVariableConfigReaderTests.cs │ ├── ErrorLevelTests.cs │ ├── FakeEmailSender.cs │ ├── FakeScmBackup.cs │ ├── FakeScmFactory.cs │ ├── FakeHosterValidator.cs │ ├── Loggers │ ├── EmailLoggerTests.cs │ └── CompositeLoggerTests.cs │ ├── FakeConfigReader.cs │ ├── FakeContext.cs │ ├── ConfigSourceTests.cs │ ├── Scm │ ├── CommandLineResultTests.cs │ ├── ScmValidatorTests.cs │ └── FakeScm.cs │ ├── LoggingHosterApiCallerTests.cs │ ├── FakeFileSystemHelper.cs │ ├── ContextTests.cs │ ├── Http │ ├── UrlHelperTests.cs │ └── HttpRequestTests.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── IgnoringHosterApiCallerTests.cs │ ├── IncludingHosterApiCallerTests.cs │ ├── ScmBackup.Tests.csproj │ ├── DeletedRepoHandlerTests.cs │ ├── TestHelperTests.cs │ ├── FakeLogger.cs │ ├── ApiCallerTests.cs │ ├── ErrorHandlingScmBackupTests.cs │ └── ValidationResultTests.cs ├── img ├── logo64x64.png ├── logo128x128.png └── logo200x200.png ├── run-all-tests.bat ├── run-unit-tests.bat ├── .gitignore ├── .editorconfig ├── cleanup.bat ├── .dockerignore ├── appveyor.yml ├── run-all-tests.ps1 ├── readme.md ├── environment-variables.ps1.sample ├── .github └── workflows │ ├── ci-linux.yml │ └── codeql-analysis.yml ├── version-number.ps1 └── ScmBackup.sln /stack-ad/.gitignore: -------------------------------------------------------------------------------- 1 | *.png -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/brokensettings.yml: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /img/logo64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianspecht/scm-backup/HEAD/img/logo64x64.png -------------------------------------------------------------------------------- /img/logo128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianspecht/scm-backup/HEAD/img/logo128x128.png -------------------------------------------------------------------------------- /img/logo200x200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christianspecht/scm-backup/HEAD/img/logo200x200.png -------------------------------------------------------------------------------- /run-all-tests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | powershell -executionpolicy bypass -file .\run-all-tests.ps1 3 | pause 4 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Scm/FakeCommandLineScmTools/FakeCommandLineScm-Command-Linux.sh: -------------------------------------------------------------------------------- 1 | echo "Test Linux" 2 | -------------------------------------------------------------------------------- /run-unit-tests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | dotnet test "%~dp0\src\ScmBackup.Tests\ScmBackup.Tests.csproj" -c Release 4 | pause -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Scm/FakeCommandLineScmTools/FakeCommandLineScm-Command-Windows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Windows -------------------------------------------------------------------------------- /src/ScmBackup/IScmBackup.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup 2 | { 3 | internal interface IScmBackup 4 | { 5 | bool Run(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | bin 4 | obj 5 | project.lock.json 6 | .vs 7 | release/ 8 | environment-variables.ps1 9 | src/ScmBackup/AssemblyCopyright.cs 10 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "diagnosticMessages": true, 3 | "longRunningTestSeconds": 120, 4 | "parallelizeTestCollections": false 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = crlf 5 | insert_final_newline = true 6 | 7 | [*.{cs,ps1}] 8 | indent_style = space 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /cleanup.bat: -------------------------------------------------------------------------------- 1 | FOR /F "tokens=*" %%G IN ('DIR /B /AD /S bin') DO RMDIR /S /Q "%%G" 2 | FOR /F "tokens=*" %%G IN ('DIR /B /AD /S obj') DO RMDIR /S /Q "%%G" 3 | rmdir /s /q "%~dp0\release" 4 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Http 2 | { 3 | internal interface IEmailSender 4 | { 5 | void Send( string subject, string body); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/settings-unknown-property.yml: -------------------------------------------------------------------------------- 1 | localFolder: 'c:\scm-backup' 2 | 3 | # this property doesn't exist in the config class: 4 | unknownProperty: 5 | foo: bar 6 | -------------------------------------------------------------------------------- /src/ScmBackup/AssemblyCopyright.tt: -------------------------------------------------------------------------------- 1 | <#@ template language="C#" #> 2 | using System.Reflection; 3 | 4 | [assembly: AssemblyCopyright("Copyright © Christian Specht 2016-<#=DateTime.Now.Year#>")] 5 | -------------------------------------------------------------------------------- /src/ScmBackup/Scm/ScmAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ScmBackup.Scm 4 | { 5 | internal class ScmAttribute : Attribute 6 | { 7 | public ScmType Type { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/IUrlHelper.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Http 2 | { 3 | internal interface IUrlHelper 4 | { 5 | string RemoveCredentialsFromUrl(string oldUrl); 6 | bool UrlIsValid(string url); 7 | } 8 | } -------------------------------------------------------------------------------- /src/ScmBackup/ScmType.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup 2 | { 3 | /// 4 | /// Supported SCMs 5 | /// 6 | internal enum ScmType 7 | { 8 | Git, 9 | Mercurial 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ScmBackup/IHosterValidator.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | 3 | namespace ScmBackup 4 | { 5 | internal interface IHosterValidator 6 | { 7 | ValidationResult Validate(ConfigSource source); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ScmBackup/CompositionRoot/IHosterFactory.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | 3 | namespace ScmBackup.CompositionRoot 4 | { 5 | internal interface IHosterFactory 6 | { 7 | IHoster Create(string hosterName); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /stack-ad/readme.md: -------------------------------------------------------------------------------- 1 | Create image ad for [Stack Overflow OSS Advertising](https://meta.stackoverflow.com/questions/349017/open-source-advertising-2017?noredirect=1&lq=1) 2 | (like [this one](https://gist.github.com/cormullion/8880c53bcb7766f50aca6c024ea845f8)) -------------------------------------------------------------------------------- /src/ScmBackup/Scm/IScmFactory.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Scm 2 | { 3 | /// 4 | /// factory to create IScm instances 5 | /// 6 | internal interface IScmFactory 7 | { 8 | IScm Create(ScmType type); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ScmBackup/IApiCaller.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup 2 | { 3 | /// 4 | /// Gets the list of repositories for each ConfigSource 5 | /// 6 | internal interface IApiCaller 7 | { 8 | ApiRepositories CallApis(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Gitlab/GitlabApiWiki.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ScmBackup.Hosters.Gitlab 6 | { 7 | class GitlabApiWiki 8 | { 9 | public string slug { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FooBarBar.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Tests.Hosters 2 | { 3 | /// 4 | /// test class for HosterNameHelperTests.ThrowsWhenTypeNameContainsSuffixMoreThanOnce 5 | /// 6 | public class FooBarBar 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ScmBackup/IHosterApiCaller.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup 6 | { 7 | internal interface IHosterApiCaller 8 | { 9 | List GetRepositoryList(ConfigSource source); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/IConfigReader.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Configuration 2 | { 3 | /// 4 | /// Reads the configuration values and returns an instance of the Config class 5 | /// 6 | internal interface IConfigReader 7 | { 8 | Config ReadConfig(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ScmBackup/IBackupMaker.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup 6 | { 7 | internal interface IBackupMaker 8 | { 9 | string Backup(ConfigSource source, IEnumerable repos); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ScmBackup/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "ScmBackup": { 4 | "commandName": "Project" 5 | }, 6 | "Docker": { 7 | "commandName": "Docker", 8 | "DockerfileRunArguments": "-v \"c:\\scm-backup:/app/backups\" -e TZ=Europe/Stockholm" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/IHoster.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters 2 | { 3 | /// 4 | /// base interface for all hosters 5 | /// 6 | internal interface IHoster 7 | { 8 | IConfigSourceValidator Validator { get; } 9 | IHosterApi Api { get; } 10 | IBackup Backup { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ScmBackup/Scm/IScmValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ScmBackup.Scm 4 | { 5 | /// 6 | /// Verifies that all passed SCMs are present on this machine 7 | /// 8 | internal interface IScmValidator 9 | { 10 | bool ValidateScms(HashSet scms); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ScmBackup/IDeletedRepoHandler.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace ScmBackup 7 | { 8 | internal interface IDeletedRepoHandler 9 | { 10 | IEnumerable HandleDeletedRepos(IEnumerable repos, string backupDir); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/IBackup.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | 3 | namespace ScmBackup.Hosters 4 | { 5 | internal interface IBackup 6 | { 7 | /// 8 | /// backup everything from this repo which needs to be backed up 9 | /// 10 | void MakeBackup(ConfigSource source, HosterRepository repo, string repoFolder); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ScmBackup/Scm/ScmCredentials.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Scm 2 | { 3 | internal class ScmCredentials 4 | { 5 | public string User { get; private set; } 6 | public string Password { get; private set; } 7 | 8 | public ScmCredentials(string user, string pass) 9 | { 10 | this.User = user; 11 | this.Password = pass; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ConfigScm.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Configuration 2 | { 3 | public class ConfigScm 4 | { 5 | /// 6 | /// Name of the SCM 7 | /// 8 | public string Name { get; set; } 9 | 10 | /// 11 | /// Path to executable 12 | /// 13 | public string Path { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ScmBackup/Program.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | 3 | namespace ScmBackup 4 | { 5 | public class Program 6 | { 7 | public static int Main(string[] args) 8 | { 9 | var container = Bootstrapper.BuildContainer(); 10 | var success = container.GetInstance().Run(); 11 | return success ? 0 : 1; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/IConfigSourceValidator.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | 3 | namespace ScmBackup.Hosters 4 | { 5 | /// 6 | /// marker interface for ConfigSource validators 7 | /// 8 | internal interface IConfigSourceValidator 9 | { 10 | bool AuthNameAndNameMustBeEqual { get; } 11 | ValidationResult Validate(ConfigSource config); 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/ScmBackup/IHosterBackupMaker.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | 4 | namespace ScmBackup 5 | { 6 | /// 7 | /// Makes a backup of one specific repository from one specific hoster 8 | /// 9 | internal interface IHosterBackupMaker 10 | { 11 | void MakeBackup(ConfigSource source, HosterRepository repo, string repoFolder); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Configuration/ConfigTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Configuration 8 | { 9 | public class ConfigTests 10 | { 11 | private Config sut; 12 | 13 | public ConfigTests() 14 | { 15 | this.sut = new Config(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ErrorLevelTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Xunit; 5 | 6 | namespace ScmBackup.Tests 7 | { 8 | public class ErrorLevelTests 9 | { 10 | [Fact] 11 | public void LevelName_ReturnsName() 12 | { 13 | var sut = ErrorLevel.Info; 14 | 15 | Assert.Equal("Info", sut.LevelName()); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/IHosterApi.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Http; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup.Hosters 6 | { 7 | /// 8 | /// marker interface for hoster's API, gets the list of repositories to clone 9 | /// 10 | internal interface IHosterApi 11 | { 12 | List GetRepositoryList(ConfigSource config); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/testsettings.yml: -------------------------------------------------------------------------------- 1 | localFolder: 'localfolder' 2 | 3 | waitSecondsOnError: 5 4 | 5 | scms: 6 | 7 | - name: git 8 | path: 'path/to/git' 9 | 10 | - name: hg 11 | path: 'path/to/hg' 12 | 13 | sources: 14 | 15 | - hoster: hoster0 16 | type: type0 17 | name: name0 18 | 19 | - hoster: hoster1 20 | type: type1 21 | name: name1 22 | ignoreRepos: 23 | - ignore0 24 | - ignore1 25 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeEmailSender.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Http; 2 | 3 | namespace ScmBackup.Tests 4 | { 5 | public class FakeEmailSender : IEmailSender 6 | { 7 | public string LastSubject { get; private set; } 8 | public string LastBody { get; private set; } 9 | 10 | public void Send(string subject, string body) 11 | { 12 | this.LastSubject = subject; 13 | this.LastBody = body; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeScmBackup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ScmBackup.Tests 4 | { 5 | public class FakeScmBackup : IScmBackup 6 | { 7 | public Exception ToThrow { get; set; } 8 | public bool ToReturn { get; set; } = true; 9 | 10 | public bool Run() 11 | { 12 | if (this.ToThrow != null) 13 | { 14 | throw this.ToThrow; 15 | } 16 | 17 | return ToReturn; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/HttpResult.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http.Headers; 3 | 4 | namespace ScmBackup.Http 5 | { 6 | /// 7 | /// return value for HttpRequest 8 | /// 9 | internal class HttpResult 10 | { 11 | public string Content { get; set; } 12 | public HttpStatusCode Status { get; set; } 13 | public bool IsSuccessStatusCode { get; set; } 14 | public HttpResponseHeaders Headers { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ScmBackup/ErrorLevel.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup 2 | { 3 | /// 4 | /// Available error levels 5 | /// 6 | internal enum ErrorLevel 7 | { 8 | Debug = 0, 9 | Info = 1, 10 | Warn = 2, 11 | Error = 3 12 | } 13 | 14 | internal static class ErrorLevelExtensions 15 | { 16 | public static string LevelName(this ErrorLevel level) 17 | { 18 | return level.ToString("f"); // https://stackoverflow.com/a/32726578/6884 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Github/GithubConfigSourceValidator.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters.Github 2 | { 3 | /// 4 | /// validator for GitHub repositories 5 | /// 6 | internal class GithubConfigSourceValidator : ConfigSourceValidatorBase 7 | { 8 | public override string HosterName 9 | { 10 | get { return "github"; } 11 | } 12 | 13 | public override bool AuthNameAndNameMustBeEqual 14 | { 15 | get { return true; } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | !**/.gitignore 27 | !.git/HEAD 28 | !.git/config 29 | !.git/packed-refs 30 | !.git/refs/heads/** -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Bitbucket/BitbucketConfigSourceValidator.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters.Bitbucket 2 | { 3 | /// 4 | /// validator for Bitbucket repositories 5 | /// 6 | internal class BitbucketConfigSourceValidator : ConfigSourceValidatorBase 7 | { 8 | public override string HosterName 9 | { 10 | get { return "bitbucket"; } 11 | } 12 | 13 | public override bool AuthNameAndNameMustBeEqual 14 | { 15 | get { return true; } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/IHttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace ScmBackup.Http 5 | { 6 | /// 7 | /// Wrapper for HttpClient 8 | /// 9 | internal interface IHttpRequest 10 | { 11 | HttpClient HttpClient { get; set; } 12 | 13 | void SetBaseUrl(string url); 14 | 15 | void AddHeader(string name, string value); 16 | 17 | void AddBasicAuthHeader(string username, string password); 18 | 19 | Task Execute(string url); 20 | } 21 | } -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Github/GithubHoster.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters.Github 2 | { 3 | internal class GithubHoster : IHoster 4 | { 5 | public GithubHoster(IConfigSourceValidator validator, IHosterApi api, IBackup backup) 6 | { 7 | this.Validator = validator; 8 | this.Api = api; 9 | this.Backup = backup; 10 | } 11 | 12 | public IConfigSourceValidator Validator { get; private set; } 13 | public IHosterApi Api { get; private set; } 14 | public IBackup Backup { get; private set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Gitlab/GitlabHoster.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters.Gitlab 2 | { 3 | internal class GitlabHoster : IHoster 4 | { 5 | public GitlabHoster(IConfigSourceValidator validator, IHosterApi api, IBackup backup) 6 | { 7 | this.Validator = validator; 8 | this.Api = api; 9 | this.Backup = backup; 10 | } 11 | 12 | public IConfigSourceValidator Validator { get; private set; } 13 | public IHosterApi Api { get; private set; } 14 | public IBackup Backup { get; private set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Bitbucket/BitbucketHoster.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters.Bitbucket 2 | { 3 | internal class BitbucketHoster : IHoster 4 | { 5 | public BitbucketHoster(IConfigSourceValidator validator, IHosterApi api, IBackup backup) 6 | { 7 | this.Validator = validator; 8 | this.Api = api; 9 | this.Backup = backup; 10 | } 11 | 12 | public IConfigSourceValidator Validator { get; private set; } 13 | public IHosterApi Api { get; private set; } 14 | public IBackup Backup { get; private set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Gitlab/GitlabApiRepo.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace ScmBackup.Hosters.Gitlab 7 | { 8 | internal class GitlabApiRepo 9 | { 10 | public int id { get; set; } 11 | public string name { get; set; } 12 | public string path_with_namespace { get; set; } 13 | public string http_url_to_repo { get; set; } 14 | public string visibility { get; set; } 15 | public bool issues_enabled { get; set; } 16 | public bool wiki_enabled { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FakeConfigSourceValidator.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | 4 | namespace ScmBackup.Tests.Hosters 5 | { 6 | internal class FakeConfigSourceValidator : IConfigSourceValidator 7 | { 8 | public bool WasValidated { get; private set; } 9 | 10 | public bool AuthNameAndNameMustBeEqual { get; } 11 | 12 | public ValidationResult Result { get; set; } 13 | 14 | public ValidationResult Validate(ConfigSource config) 15 | { 16 | this.WasValidated = true; 17 | return this.Result; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ScmBackup/IContext.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | 4 | namespace ScmBackup 5 | { 6 | /// 7 | /// "application context" for global information 8 | /// 9 | internal interface IContext 10 | { 11 | Version VersionNumber { get; } 12 | 13 | string VersionNumberString { get; } 14 | 15 | string AppTitle { get; } 16 | 17 | /// 18 | /// "short version" of the app title, valid for HTTP user agent 19 | /// 20 | string UserAgent { get; } 21 | 22 | Config Config { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Gitlab/GitlabConfigSourceValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ScmBackup.Hosters.Gitlab 6 | { 7 | /// 8 | /// validator for GitLab config sources 9 | /// 10 | internal class GitlabConfigSourceValidator : ConfigSourceValidatorBase 11 | { 12 | public override string HosterName 13 | { 14 | get { return "gitlab"; } 15 | } 16 | 17 | public override bool AuthNameAndNameMustBeEqual 18 | { 19 | get { return true; } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FakeHosterApi.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using ScmBackup.Http; 4 | using System.Collections.Generic; 5 | 6 | namespace ScmBackup.Tests.Hosters 7 | { 8 | internal class FakeHosterApi : IHosterApi 9 | { 10 | public bool WasCalled { get; private set;} 11 | 12 | public List RepoList { get; set; } 13 | 14 | public HttpResult LastResult { get; set; } 15 | 16 | public List GetRepositoryList(ConfigSource config) 17 | { 18 | this.WasCalled = true; 19 | return this.RepoList; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeScmFactory.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup.Tests 6 | { 7 | internal class FakeScmFactory : Dictionary, IScmFactory 8 | { 9 | public void Register(ScmType type, IScm scm) 10 | { 11 | this.Add(type, scm); 12 | } 13 | 14 | public IScm Create(ScmType type) 15 | { 16 | IScm result; 17 | if (!this.TryGetValue(type, out result)) 18 | { 19 | throw new InvalidOperationException(); 20 | } 21 | return result; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/IConfigBackupMaker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ScmBackup.Configuration 4 | { 5 | internal interface IConfigBackupMaker 6 | { 7 | /// 8 | /// subfolder where the configs are saved 9 | /// 10 | string SubFolder { get; } 11 | 12 | /// 13 | /// File names of the config files to backup 14 | /// 15 | List ConfigFileNames { get; } 16 | 17 | /// 18 | /// Copies important config files into the backup folder. 19 | /// 20 | void BackupConfigs(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/CloneUrlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace ScmBackup.Tests.Hosters 4 | { 5 | public class CloneUrlBuilderTests 6 | { 7 | [Fact] 8 | public void BuildsGithubCloneUrl() 9 | { 10 | var result = CloneUrlBuilder.GithubCloneUrl("foo", "bar"); 11 | 12 | Assert.Equal("https://github.com/foo/bar", result); 13 | } 14 | 15 | [Fact] 16 | public void BuildsBitbucketCloneUrl() 17 | { 18 | var result = CloneUrlBuilder.BitbucketCloneUrl("foo", "bar"); 19 | 20 | Assert.Equal("https://bitbucket.org/foo/bar", result); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/GithubConfigSourceValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters.Github; 3 | 4 | namespace ScmBackup.Tests.Hosters 5 | { 6 | public class GithubConfigSourceValidatorTests : IConfigSourceValidatorTests 7 | { 8 | public GithubConfigSourceValidatorTests() 9 | { 10 | config = new ConfigSource(); 11 | config.Hoster = "github"; 12 | config.Type = "user"; 13 | config.Name = "foo"; 14 | config.AuthName = config.Name; 15 | config.Password = "pass"; 16 | 17 | sut = new GithubConfigSourceValidator(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ScmBackup/ValidationMessageType.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup 2 | { 3 | /// 4 | /// Used to distinguish between different validation messages. 5 | /// Not really elegant, but the only way to test whether the validation returned a specific message. 6 | /// 7 | public enum ValidationMessageType 8 | { 9 | WrongHoster, 10 | WrongType, 11 | NameEmpty, 12 | AuthNameOrPasswortEmpty, 13 | AuthNameAndPasswortEmpty, 14 | AuthNameAndNameNotEqual, 15 | 16 | /// 17 | /// default value when not set - sometimes we don't care 18 | /// 19 | Undefined 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/BitbucketConfigSourceValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters.Bitbucket; 3 | 4 | namespace ScmBackup.Tests.Hosters 5 | { 6 | public class BitbucketConfigSourceValidatorTests : IConfigSourceValidatorTests 7 | { 8 | public BitbucketConfigSourceValidatorTests() 9 | { 10 | config = new ConfigSource(); 11 | config.Hoster = "bitbucket"; 12 | config.Type = "user"; 13 | config.Name = "foo"; 14 | config.AuthName = config.Name; 15 | config.Password = "pass"; 16 | 17 | sut = new BitbucketConfigSourceValidator(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/BackupBaseTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using Xunit; 4 | 5 | namespace ScmBackup.Tests.Hosters 6 | { 7 | public class BackupBaseTests 8 | { 9 | [Fact] 10 | public void BackupBaseExecutesAllSubMethods() 11 | { 12 | var repo = new HosterRepository("foo", "foo", "http://clone", ScmType.Git); 13 | repo.SetWiki(true, "http://wiki"); 14 | repo.SetIssues(true, "http://issues"); 15 | 16 | var sut = new FakeHosterBackup(); 17 | sut.MakeBackup(new ConfigSource(), repo, @"c:\foo"); 18 | 19 | Assert.True(sut.WasExecuted); 20 | } 21 | 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ConfigOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ScmBackup.Configuration 6 | { 7 | /// 8 | /// Main class for various options 9 | /// 10 | class ConfigOptions 11 | { 12 | public BackupOptions Backup { get; set; } = new BackupOptions(); 13 | } 14 | 15 | class BackupOptions 16 | { 17 | public bool RemoveDeletedRepos { get; set; } 18 | public bool LogRepoFinished { get; set; } 19 | public bool AddTimestampedSubfolder { get; set; } 20 | public string TimestampFormat { get; set; } 21 | public string s3BucketName { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Github/GithubBackup.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using System; 3 | 4 | namespace ScmBackup.Hosters.Github 5 | { 6 | internal class GithubBackup : BackupBase 7 | { 8 | public GithubBackup(IScmFactory scmfactory) 9 | { 10 | this.scmFactory = scmfactory; 11 | } 12 | 13 | public override void BackupRepo(string subdir, ScmCredentials credentials) 14 | { 15 | this.DefaultBackup(this.repo.CloneUrl, subdir, credentials); 16 | } 17 | 18 | public override void BackupWiki(string subdir, ScmCredentials credentials) 19 | { 20 | this.DefaultBackup(this.repo.WikiUrl, subdir, credentials); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/GitlabConfigSourceValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters.Gitlab; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace ScmBackup.Tests.Hosters 8 | { 9 | public class GitlabConfigSourceValidatorTests : IConfigSourceValidatorTests 10 | { 11 | public GitlabConfigSourceValidatorTests() 12 | { 13 | config = new ConfigSource(); 14 | config.Hoster = "gitlab"; 15 | config.Type = "user"; 16 | config.Name = "foo"; 17 | config.AuthName = config.Name; 18 | config.Password = "pass"; 19 | 20 | sut = new GitlabConfigSourceValidator(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ScmBackup/NLog.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeHosterValidator.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | 3 | namespace ScmBackup.Tests 4 | { 5 | internal class FakeHosterValidator : IHosterValidator 6 | { 7 | public int ValidationCounter { get; private set; } 8 | public bool WasValidated { get; private set; } 9 | public ValidationResult Result { get; set; } 10 | 11 | public FakeHosterValidator() 12 | { 13 | this.Result = new ValidationResult(); 14 | this.ValidationCounter = 0; 15 | } 16 | 17 | public ValidationResult Validate(ConfigSource config) 18 | { 19 | this.WasValidated = true; 20 | this.ValidationCounter++; 21 | return this.Result; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ScmBackup/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ScmBackup 5 | { 6 | /// 7 | /// Interface for logging 8 | /// 9 | internal interface ILogger 10 | { 11 | void Log(ErrorLevel level, string message, params object[] arg); 12 | void Log(ErrorLevel level, Exception ex, string message, params object[] arg); 13 | 14 | /// 15 | /// List of files to backup (for example, the logger's config file) 16 | /// 17 | List FilesToBackup { get; } 18 | 19 | /// 20 | /// This will be executed when SCM Backup exits 21 | /// 22 | void ExecuteOnExit(bool successful); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Gitlab/GitlabBackup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using ScmBackup.Scm; 5 | 6 | namespace ScmBackup.Hosters.Gitlab 7 | { 8 | internal class GitlabBackup : BackupBase 9 | { 10 | public GitlabBackup(IScmFactory scmfactory) 11 | { 12 | this.scmFactory = scmfactory; 13 | } 14 | 15 | public override void BackupRepo(string subdir, ScmCredentials credentials) 16 | { 17 | this.DefaultBackup(this.repo.CloneUrl, subdir, credentials); 18 | } 19 | 20 | public override void BackupWiki(string subdir, ScmCredentials credentials) 21 | { 22 | this.DefaultBackup(this.repo.WikiUrl, subdir, credentials); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ScmBackup/CompositionRoot/HosterValidator.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | 4 | namespace ScmBackup.CompositionRoot 5 | { 6 | internal class HosterValidator : IHosterValidator 7 | { 8 | private readonly IHosterFactory factory; 9 | 10 | public HosterValidator(IHosterFactory factory) 11 | { 12 | this.factory = factory; 13 | } 14 | 15 | public ValidationResult Validate(ConfigSource source) 16 | { 17 | if (source == null) 18 | { 19 | throw new ArgumentNullException(Resource.ConfigSourceIsNull); 20 | } 21 | 22 | var hoster = this.factory.Create(source.Hoster); 23 | return hoster.Validator.Validate(source); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Bitbucket/BitbucketBackup.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace ScmBackup.Hosters.Bitbucket 7 | { 8 | internal class BitbucketBackup : BackupBase 9 | { 10 | public BitbucketBackup(IScmFactory scmfactory) 11 | { 12 | this.scmFactory = scmfactory; 13 | } 14 | 15 | public override void BackupRepo(string subdir, ScmCredentials credentials) 16 | { 17 | this.DefaultBackup(this.repo.CloneUrl, subdir, credentials); 18 | } 19 | 20 | public override void BackupWiki(string subdir, ScmCredentials credentials) 21 | { 22 | this.DefaultBackup(this.repo.WikiUrl, subdir, credentials); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/CloneUrlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Tests.Hosters 2 | { 3 | /// 4 | /// Helper class to build clone URLs for various hosters 5 | /// Not part of IHoster and its subclasses by purpose, so we can use it in the tests without needing to create a complete IHoster instance. 6 | /// 7 | public static class CloneUrlBuilder 8 | { 9 | public static string GithubCloneUrl(string username, string reponame) 10 | { 11 | return string.Format("https://github.com/{0}/{1}", username, reponame); 12 | } 13 | 14 | public static string BitbucketCloneUrl(string username, string reponame) 15 | { 16 | return string.Format("https://bitbucket.org/{0}/{1}", username, reponame); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ScmBackup/LoggingHosterApiCaller.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ScmBackup 7 | { 8 | internal class LoggingHosterApiCaller : IHosterApiCaller 9 | { 10 | private readonly IHosterApiCaller caller; 11 | private readonly ILogger logger; 12 | 13 | public LoggingHosterApiCaller(IHosterApiCaller caller, ILogger logger) 14 | { 15 | this.caller = caller; 16 | this.logger = logger; 17 | } 18 | 19 | public List GetRepositoryList(ConfigSource source) 20 | { 21 | this.logger.Log(ErrorLevel.Info, Resource.ApiGettingRepos, source.Title, source.Hoster); 22 | return this.caller.GetRepositoryList(source); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ScmBackup/CompositionRoot/HosterApiCaller.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace ScmBackup.CompositionRoot 7 | { 8 | internal class HosterApiCaller : IHosterApiCaller 9 | { 10 | private readonly IHosterFactory factory; 11 | 12 | public HosterApiCaller(IHosterFactory factory) 13 | { 14 | this.factory = factory; 15 | } 16 | 17 | public List GetRepositoryList(ConfigSource source) 18 | { 19 | if (source == null) 20 | { 21 | throw new ArgumentNullException(Resource.ConfigSourceIsNull); 22 | } 23 | 24 | var hoster = this.factory.Create(source.Hoster); 25 | return hoster.Api.GetRepositoryList(source); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ScmBackup/IgnoringHosterApiCaller.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ScmBackup 7 | { 8 | internal class IgnoringHosterApiCaller : IHosterApiCaller 9 | { 10 | private readonly IHosterApiCaller caller; 11 | 12 | public IgnoringHosterApiCaller(IHosterApiCaller caller) 13 | { 14 | this.caller = caller; 15 | } 16 | 17 | public List GetRepositoryList(ConfigSource source) 18 | { 19 | var list = this.caller.GetRepositoryList(source); 20 | 21 | if (source.IgnoreRepos != null && source.IgnoreRepos.Any()) 22 | { 23 | list.RemoveAll(l => source.IgnoreRepos.Contains(l.ShortName)); 24 | } 25 | 26 | return list; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ConfigEmail.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ScmBackup.Configuration 4 | { 5 | /// 6 | /// Configuration data for email sending 7 | /// 8 | public class ConfigEmail 9 | { 10 | public string From { get; set; } 11 | public string To { get; set; } 12 | public string Server { get; set; } 13 | public int Port { get; set; } 14 | public bool UseSsl { get; set; } 15 | public string UserName { get; set; } 16 | public string Password { get; set; } 17 | 18 | /// 19 | /// The "To" value can contain multiple emails separated with ; 20 | /// This will return a list of them 21 | /// 22 | public IEnumerable To_AsList() 23 | { 24 | return this.To.Split(";"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ScmBackup/IncludingHosterApiCaller.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace ScmBackup 7 | { 8 | internal class IncludingHosterApiCaller : IHosterApiCaller 9 | { 10 | private readonly IHosterApiCaller caller; 11 | 12 | public IncludingHosterApiCaller(IHosterApiCaller caller) 13 | { 14 | this.caller = caller; 15 | } 16 | 17 | public List GetRepositoryList(ConfigSource source) 18 | { 19 | var list = this.caller.GetRepositoryList(source); 20 | 21 | if (source.IncludeRepos != null && source.IncludeRepos.Any()) 22 | { 23 | list.RemoveAll(l => !source.IncludeRepos.Contains(l.ShortName)); 24 | } 25 | 26 | return list; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | image: Visual Studio 2022 3 | install: 4 | - ps: C:\projects\scm-backup\version-number.ps1 5 | build_script: 6 | - ps: C:\projects\scm-backup\build-release.ps1 7 | test: off 8 | artifacts: 9 | - path: release\scm-backup-*.zip 10 | name: Application 11 | - path: src\ScmBackup.Tests.Integration\bin\Release\net8.0\*.log 12 | name: Integration Test Logfile 13 | assembly_info: 14 | patch: true 15 | file: AssemblyInfo.cs 16 | assembly_version: '$(ScmBackupShortVersion)' 17 | assembly_file_version: '$(ScmBackupShortVersion)' 18 | assembly_informational_version: '$(ScmBackupLongVersion)' 19 | deploy: 20 | description: '' 21 | provider: GitHub 22 | auth_token: 23 | secure: jRqXr7hU3p3AEzsOemTcWbDSAgNuooWgME3GQHAxtDjxFcGHiLFhBeaRbighBLLZ 24 | artifact: Application 25 | draft: false 26 | prerelease: false 27 | on: 28 | branch: master 29 | appveyor_repo_tag: true -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Hosters/HttpLogHelper.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Http; 2 | 3 | namespace ScmBackup.Tests.Integration.Hosters 4 | { 5 | /// 6 | /// Helper to test Http requests with logging 7 | /// 8 | internal class HttpLogHelper 9 | { 10 | /// 11 | /// Returns an IHttpRequest which is set up to log via TestLogger 12 | /// 13 | public static IHttpRequest GetRequest(string logName) 14 | { 15 | return new LoggingHttpRequest(new HttpRequest(), new TestLogger(logName)); 16 | } 17 | 18 | /// 19 | /// Returns an IHttpRequest which is set up to log via the passed ILogger 20 | /// 21 | public static IHttpRequest GetRequest(ILogger logger) 22 | { 23 | return new LoggingHttpRequest(new HttpRequest(), logger); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Loggers/EmailLoggerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Loggers; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Loggers 8 | { 9 | public class EmailLoggerTests 10 | { 11 | [Fact] 12 | public void SendsMailWithLogs() 13 | { 14 | var mail = new FakeEmailSender(); 15 | var sut = new EmailLogger(mail); 16 | 17 | sut.Log(ErrorLevel.Debug, "AAAA"); 18 | sut.Log(ErrorLevel.Info, "BBBB"); 19 | sut.Log(ErrorLevel.Error, "CCCC"); 20 | sut.ExecuteOnExit(true); 21 | 22 | Assert.NotNull(mail.LastSubject); 23 | Assert.NotNull(mail.LastBody); 24 | Assert.DoesNotContain("AAAA", mail.LastBody); 25 | Assert.Contains("BBBB", mail.LastBody); 26 | Assert.Contains("CCCC", mail.LastBody); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ScmBackup.Configuration 5 | { 6 | /// 7 | /// Holds all configuration values 8 | /// 9 | internal class Config 10 | { 11 | public string LocalFolder { get; set; } 12 | 13 | public int WaitSecondsOnError { get; set; } 14 | 15 | public List Scms { get; set; } 16 | 17 | public List Sources { get; set; } 18 | 19 | /// 20 | /// Various options 21 | /// 22 | public ConfigOptions Options { get; set; } 23 | 24 | public ConfigEmail Email { get; set; } 25 | 26 | public Config() 27 | { 28 | this.Sources = new List(); 29 | this.Scms = new List(); 30 | this.Options = new ConfigOptions(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeConfigReader.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | 4 | namespace ScmBackup.Tests 5 | { 6 | internal class FakeConfigReader : IConfigReader 7 | { 8 | public Config FakeConfig { get; set; } 9 | 10 | public Config ReadConfig() 11 | { 12 | if (this.FakeConfig == null) 13 | { 14 | throw new InvalidOperationException(); 15 | } 16 | 17 | return this.FakeConfig; 18 | } 19 | 20 | public void SetDefaultFakeConfig() 21 | { 22 | var config = new Config(); 23 | config.LocalFolder = "foo"; 24 | config.WaitSecondsOnError = 0; 25 | 26 | var source = new ConfigSource(); 27 | source.Title = "title"; 28 | source.Hoster = "fake"; 29 | 30 | config.Sources.Add(source); 31 | 32 | this.FakeConfig = config; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FakeHosterFactory.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using ScmBackup.Hosters; 3 | using System; 4 | 5 | namespace ScmBackup.Tests.Hosters 6 | { 7 | internal class FakeHosterFactory : IHosterFactory 8 | { 9 | public IHoster FakeHoster { get; set; } 10 | public string LastHosterName { get; private set; } 11 | public bool CreateWasCalled { get; private set; } 12 | 13 | public FakeHosterFactory() { } 14 | 15 | public FakeHosterFactory(IHoster hoster) 16 | { 17 | this.FakeHoster = hoster; 18 | } 19 | 20 | public IHoster Create(string hosterName) 21 | { 22 | this.CreateWasCalled = true; 23 | this.LastHosterName = hosterName; 24 | 25 | if (this.FakeHoster == null) 26 | { 27 | throw new InvalidOperationException(); 28 | } 29 | 30 | return this.FakeHoster; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/UrlHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ScmBackup.Http 4 | { 5 | internal class UrlHelper : IUrlHelper 6 | { 7 | public bool UrlIsValid(string url) 8 | { 9 | // https://stackoverflow.com/a/7581824/6884 10 | // (Uri.UriSchemeHttp and Uri.UriSchemeHttps replaced by strings because apparently they don't exist in .NET Core) 11 | Uri uriResult; 12 | return Uri.TryCreate(url, UriKind.Absolute, out uriResult) 13 | && (uriResult.Scheme == "http" || uriResult.Scheme == "https"); 14 | } 15 | 16 | public string RemoveCredentialsFromUrl(string oldUrl) 17 | { 18 | var uri = new UriBuilder(oldUrl); 19 | uri.UserName = null; 20 | uri.Password = null; 21 | if (uri.Uri.IsDefaultPort) 22 | { 23 | uri.Port = -1; 24 | } 25 | return uri.Uri.AbsoluteUri; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ScmBackup/CompositionRoot/HosterBackupMaker.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System; 4 | 5 | namespace ScmBackup.CompositionRoot 6 | { 7 | /// 8 | /// Makes a backup of one specific repository from one specific hoster 9 | /// 10 | internal class HosterBackupMaker : IHosterBackupMaker 11 | { 12 | private readonly IHosterFactory factory; 13 | 14 | public HosterBackupMaker(IHosterFactory factory) 15 | { 16 | this.factory = factory; 17 | } 18 | 19 | public void MakeBackup(ConfigSource source, HosterRepository repo, string repoFolder) 20 | { 21 | if (source == null) 22 | { 23 | throw new ArgumentNullException(Resource.ConfigSourceIsNull); 24 | } 25 | 26 | var hoster = factory.Create(source.Hoster); 27 | hoster.Backup.MakeBackup(source, repo, repoFolder); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /run-all-tests.ps1: -------------------------------------------------------------------------------- 1 | if (Test-Path -Path $PSScriptRoot\environment-variables.ps1) { 2 | & $PSScriptRoot\environment-variables.ps1 3 | } 4 | else { 5 | Write-Host 'environment-variables.ps1 is missing. Some of the integration tests will fail!' 6 | Write-Host 'Press ENTER to continue' 7 | Read-Host 8 | } 9 | 10 | 11 | 12 | '' 13 | Write-Host '###### DELETING OLD TEMP FOLDERS ######' 14 | $temppath = "$env:TEMP\_scm-backup-tests\" 15 | if (Test-Path -Path $temppath) { 16 | Get-ChildItem -Path $temppath -Recurse| Foreach-object {Remove-item -Recurse -Force -path $_.FullName } 17 | } 18 | 19 | 20 | 21 | '' 22 | Write-Host '###### UNIT TESTS ######' 23 | dotnet test "$PSScriptRoot\src\ScmBackup.Tests\ScmBackup.Tests.csproj" -c Release 24 | if ($LASTEXITCODE -eq 1) { 25 | throw 26 | } 27 | 28 | 29 | '' 30 | Write-Host '###### INTEGRATION TESTS ######' 31 | dotnet test "$PSScriptRoot\src\ScmBackup.Tests.Integration\ScmBackup.Tests.Integration.csproj" -c Release 32 | if ($LASTEXITCODE -eq 1) { 33 | throw 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/Bitbucket/BitbucketApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ScmBackup.Hosters.Bitbucket 4 | { 5 | internal class BitbucketApiResponse 6 | { 7 | public List values { get; set; } 8 | public string next { get; set; } 9 | 10 | internal class Repo 11 | { 12 | public string scm { get; set; } 13 | public string slug { get; set; } 14 | public string full_name { get; set; } 15 | public bool has_wiki { get; set; } 16 | public bool has_issues { get; set; } 17 | public bool is_private { get; set; } 18 | public Links links { get; set; } 19 | 20 | internal class Links 21 | { 22 | public List clone { get; set; } 23 | 24 | internal class Clone 25 | { 26 | public string href { get; set; } 27 | public string name { get; set; } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FakeHosterApiCaller.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace ScmBackup.Tests.Hosters 7 | { 8 | internal class FakeHosterApiCaller : IHosterApiCaller 9 | { 10 | public Dictionary> Lists { get; private set; } 11 | public List PassedConfigSources { get; private set; } 12 | 13 | public FakeHosterApiCaller() 14 | { 15 | this.Lists = new Dictionary>(); 16 | this.PassedConfigSources = new List(); 17 | } 18 | 19 | public List GetRepositoryList(ConfigSource source) 20 | { 21 | if (this.Lists == null || this.Lists.Count == 0) 22 | { 23 | throw new InvalidOperationException("dictionary is empty"); 24 | } 25 | 26 | this.PassedConfigSources.Add(source); 27 | return this.Lists[source]; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeContext.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | 4 | namespace ScmBackup.Tests 5 | { 6 | internal class FakeContext : IContext 7 | { 8 | public FakeContext() 9 | { 10 | this.VersionNumber = new Version(0, 0, 0); 11 | this.VersionNumberString = this.VersionNumber.ToString(); 12 | this.AppTitle = "SCM Backup"; 13 | this.UserAgent = "SCM-Backup"; 14 | 15 | var reader = new FakeConfigReader(); 16 | reader.SetDefaultFakeConfig(); 17 | this.Config = reader.ReadConfig(); 18 | } 19 | 20 | public Version VersionNumber { get; set; } 21 | 22 | public string VersionNumberString { get; set; } 23 | 24 | public string AppTitle { get; set; } 25 | 26 | public string UserAgent { get; set; } 27 | 28 | public Config Config { get; set; } 29 | 30 | public static FakeContext BuildFakeContextWithConfig(Config config) 31 | { 32 | var context = new FakeContext(); 33 | context.Config = config; 34 | return context; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Configuration/ConfigEmailTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Xunit; 7 | 8 | namespace ScmBackup.Tests.Configuration 9 | { 10 | public class ConfigEmailTests 11 | { 12 | [Fact] 13 | public void ListContainsSingleEmail() 14 | { 15 | var sut = new ConfigEmail(); 16 | sut.To = "foo@example.com"; 17 | 18 | var result = sut.To_AsList(); 19 | 20 | Assert.Single(result); 21 | Assert.Equal("foo@example.com", result.First()); 22 | } 23 | 24 | [Fact] 25 | public void ListContainsMultipleEmails() 26 | { 27 | var sut = new ConfigEmail(); 28 | sut.To = "1@example.com;2@example.com;3@example.com"; 29 | 30 | var result = sut.To_AsList(); 31 | Assert.Equal(3, result.Count()); 32 | Assert.Contains("1@example.com", result); 33 | Assert.Contains("2@example.com", result); 34 | Assert.Contains("3@example.com", result); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FakeHosterBackup.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using ScmBackup.Scm; 3 | 4 | namespace ScmBackup.Tests.Hosters 5 | { 6 | internal class FakeHosterBackup : BackupBase 7 | { 8 | private bool issuesWasExecuted; 9 | private bool repoWasExecuted; 10 | private bool wikiWasExecuted; 11 | 12 | public FakeHosterBackup() 13 | { 14 | this.scmFactory = new FakeScmFactory(); 15 | } 16 | 17 | public bool WasExecuted 18 | { 19 | get 20 | { 21 | return this.issuesWasExecuted && this.repoWasExecuted && this.wikiWasExecuted; 22 | } 23 | } 24 | 25 | public override void BackupIssues(string subdir, ScmCredentials credentials) 26 | { 27 | this.issuesWasExecuted = true; 28 | } 29 | 30 | public override void BackupRepo(string subdir, ScmCredentials credentials) 31 | { 32 | this.repoWasExecuted = true; 33 | } 34 | 35 | public override void BackupWiki(string subdir, ScmCredentials credentials) 36 | { 37 | this.wikiWasExecuted = true; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SCM Backup 2 | 3 | ![SCM Backup logo](https://scm-backup.org/img/logo128x128.png) 4 | 5 | [![Windows Build status](https://ci.appveyor.com/api/projects/status/a28uyjw91iim9wv9?svg=true)](https://ci.appveyor.com/project/ChristianSpecht/scm-backup) 6 | [![Linux Build status](https://github.com/christianspecht/scm-backup/actions/workflows/ci-linux.yml/badge.svg)](https://github.com/christianspecht/scm-backup/actions/workflows/ci-linux.yml) 7 | 8 | SCM Backup is a tool which makes offline backups of your cloud hosted source code repositories, by cloning them. It supports backing up from multiple source code hosters and backing up multiple users/teams per source code hoster. 9 | At the moment, the following hosters are supported: 10 | 11 | - [Bitbucket](https://bitbucket.org) 12 | - [GitHub](https://github.com) 13 | - [GitLab](https://gitlab.com) 14 | 15 | 16 | And it's written in [.NET Core](https://dotnet.github.io/), which means that it runs on Windows, Linux **and** MacOS. 17 | 18 | 19 | - [Website](https://scm-backup.org) 20 | - [Documentation](https://docs.scm-backup.org) 21 | - [Download](https://scm-backup.org/downloads/) 22 | 23 | 24 | ## License 25 | 26 | SCM Backup is licensed under the GNU GPLv3. -------------------------------------------------------------------------------- /src/ScmBackup/LoggingScmBackup.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace ScmBackup 4 | { 5 | internal class LoggingScmBackup : IScmBackup 6 | { 7 | private readonly IScmBackup backup; 8 | private readonly IContext context; 9 | private readonly ILogger logger; 10 | 11 | public LoggingScmBackup(IScmBackup backup, IContext context, ILogger logger) 12 | { 13 | this.backup = backup; 14 | this.context = context; 15 | this.logger = logger; 16 | } 17 | 18 | public bool Run() 19 | { 20 | logger.Log(ErrorLevel.Info, this.context.AppTitle); 21 | logger.Log(ErrorLevel.Info, Resource.AppWebsite); 22 | logger.Log(ErrorLevel.Info, string.Format(Resource.SystemOS, RuntimeInformation.OSDescription)); 23 | // TODO: log more stuff (configuration...) 24 | 25 | var result = this.backup.Run(); 26 | 27 | logger.Log(ErrorLevel.Info, Resource.BackupFinished); 28 | logger.Log(ErrorLevel.Info, string.Format(Resource.BackupFinishedDirectory, this.context.Config.LocalFolder)); 29 | 30 | return result; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ScmBackup/ApiCaller.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ScmBackup 4 | { 5 | /// 6 | /// Gets the list of repositories for each ConfigSource 7 | /// 8 | internal class ApiCaller : IApiCaller 9 | { 10 | private readonly IHosterApiCaller apiCaller; 11 | private readonly IContext context; 12 | 13 | public ApiCaller(IHosterApiCaller apiCaller, IContext context) 14 | { 15 | if (apiCaller == null) 16 | { 17 | throw new InvalidOperationException("apiCaller is null"); 18 | } 19 | 20 | if (context == null) 21 | { 22 | throw new InvalidOperationException("context is null"); 23 | } 24 | 25 | this.apiCaller = apiCaller; 26 | this.context = context; 27 | } 28 | 29 | public ApiRepositories CallApis() 30 | { 31 | var repos = new ApiRepositories(); 32 | 33 | foreach (var source in this.context.Config.Sources) 34 | { 35 | var tmp = this.apiCaller.GetRepositoryList(source); 36 | repos.AddItem(source, tmp); 37 | } 38 | 39 | return repos; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /environment-variables.ps1.sample: -------------------------------------------------------------------------------- 1 | # copy/rename this file to environment-variables.ps1 2 | 3 | Write-Host 'Setting environment variables for integration tests...' 4 | 5 | $env:Tests_Github_Name = 'scm-backup-testuser' # User for authentication. Must have read permission for private repo. 6 | $env:Tests_Github_PW = 'not-the-real-password' # the user's personal access token 7 | $env:Tests_Github_RepoPrivate = 'scm-backup-test-private' # a private repository 8 | 9 | $env:Tests_Bitbucket_Name = 'scm-backup-testuser' # User for authentication. Must have read permission for private repo. 10 | $env:Tests_Bitbucket_PW = 'not-the-real-password' # the user's app password 11 | $env:Tests_Bitbucket_RepoPrivateGit = 'scm-backup-test-private-git' # a private repository 12 | 13 | $env:Tests_Gitlab_Name = 'scm-backup-testuser' # User for authentication. Must have read permission for private repo. 14 | $env:Tests_Gitlab_PW = 'not-the-real-password' # the user's personal access token 15 | $env:Tests_Gitlab_RepoPrivate = 'scm-backup-test-private' # a private repository 16 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/ScmFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using ScmBackup.Scm; 3 | using SimpleInjector; 4 | using System; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Integration 8 | { 9 | public class ScmFactoryTests 10 | { 11 | private readonly ScmFactory sut; 12 | 13 | public ScmFactoryTests() 14 | { 15 | var container = new Container(); 16 | container.Register(); 17 | container.Register(); 18 | 19 | sut = new ScmFactory(container); 20 | sut.Register(typeof(GitScm)); 21 | } 22 | 23 | [Fact] 24 | public void NewScmIsAdded() 25 | { 26 | Assert.Single(sut); 27 | } 28 | 29 | [Fact] 30 | public void CreateReturnScm() 31 | { 32 | var result = sut.Create(ScmType.Git); 33 | 34 | Assert.NotNull(result); 35 | Assert.True(result is IScm); 36 | } 37 | 38 | [Fact] 39 | public void RegisterThrowsIfRegisteredTypeIsNotIScm() 40 | { 41 | Assert.Throws(() => sut.Register(typeof(ScmBackup))); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ConfigSourceTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using Xunit; 3 | 4 | namespace ScmBackup.Tests 5 | { 6 | public class ConfigSourceTests 7 | { 8 | [Fact] 9 | public void ConfigSourcesWithSameTitleAreEqual() 10 | { 11 | var source1 = new ConfigSource(); 12 | source1.Title = "foo"; 13 | 14 | var source2 = new ConfigSource(); 15 | source2.Title = "foo"; 16 | 17 | Assert.True(source1.Equals(source2), "Equals"); 18 | Assert.True(object.Equals(source1, source2), "object.Equals"); 19 | Assert.True(source1.GetHashCode() == source2.GetHashCode(), "GetHashCode"); 20 | } 21 | 22 | [Theory] 23 | [InlineData("", "", false)] 24 | [InlineData("foo", "", false)] 25 | [InlineData("", "bar", false)] 26 | [InlineData("foo", "bar", true)] 27 | public void IsAuthenticatedWorks(string authName, string password, bool expectedValue) 28 | { 29 | var sut = new ConfigSource(); 30 | sut.Title = "x"; 31 | sut.AuthName = authName; 32 | sut.Password = password; 33 | 34 | Assert.Equal(expectedValue, sut.IsAuthenticated); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/HosterValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using System; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace ScmBackup.Tests.Hosters 7 | { 8 | public class HosterValidatorTests 9 | { 10 | [Fact] 11 | public void ValidateCallsUnderlyingValidator() 12 | { 13 | var reader = new FakeConfigReader(); 14 | reader.SetDefaultFakeConfig(); 15 | var config = reader.ReadConfig(); 16 | var source = config.Sources.First(); 17 | 18 | var factory = new FakeHosterFactory(); 19 | var hoster = new FakeHoster(); 20 | factory.FakeHoster = hoster; 21 | 22 | var sut = new HosterValidator(factory); 23 | sut.Validate(source); 24 | 25 | Assert.True(hoster.FakeValidator.WasValidated); 26 | } 27 | 28 | [Fact] 29 | public void ThrowsWhenConfigSourceIsNull() 30 | { 31 | var factory = new FakeHosterFactory(); 32 | var hoster = new FakeHoster(); 33 | factory.FakeHoster = hoster; 34 | 35 | var sut = new HosterValidator(factory); 36 | Assert.Throws(() => sut.Validate(null)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ScmBackup/IFileSystemHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ScmBackup 4 | { 5 | /// 6 | /// helper class for file system operations 7 | /// 8 | public interface IFileSystemHelper 9 | { 10 | /// 11 | /// Checks whether the given directory is empty 12 | /// 13 | bool DirectoryIsEmpty(string path); 14 | 15 | /// 16 | /// wrapper for Directory.CreateDirectory 17 | /// 18 | void CreateDirectory(string path); 19 | 20 | /// 21 | /// Creates a subdirectory inside the given directory and returns the path 22 | /// 23 | string CreateSubDirectory(string mainDir, string subDir); 24 | 25 | /// 26 | /// wrapper for Path.Combine 27 | /// 28 | string PathCombine(string path1, string path2); 29 | 30 | /// 31 | /// Returns a list of all subdirectory names 32 | /// 33 | IEnumerable GetSubDirectoryNames(string path); 34 | 35 | /// 36 | /// Deletes a directory 37 | /// 38 | void DeleteDirectory(string path); 39 | } 40 | } -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Scm/CommandLineResultTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using Xunit; 3 | 4 | namespace ScmBackup.Tests.Scm 5 | { 6 | public class CommandLineResultTests 7 | { 8 | [Fact] 9 | public void OutputReturnsStandard() 10 | { 11 | var sut = new CommandLineResult(); 12 | sut.StandardOutput = "foo"; 13 | 14 | Assert.Equal(sut.StandardOutput, sut.Output); 15 | } 16 | 17 | [Fact] 18 | public void OutputReturnsError() 19 | { 20 | var sut = new CommandLineResult(); 21 | sut.StandardError = "foo"; 22 | 23 | Assert.Equal(sut.StandardError, sut.Output); 24 | } 25 | 26 | [Fact] 27 | public void NewInstanceReturnsNotSuccessful() 28 | { 29 | var sut = new CommandLineResult(); 30 | Assert.False(sut.Successful); 31 | } 32 | 33 | [Theory] 34 | [InlineData(true, 0)] 35 | [InlineData(false, 1)] 36 | public void SuccessfulReturnsCorrectValue(bool expected, int exitcode) 37 | { 38 | var sut = new CommandLineResult(); 39 | sut.ExitCode = exitcode; 40 | 41 | Assert.Equal(expected, sut.Successful); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/HosterApiCallerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using System; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace ScmBackup.Tests.Hosters 7 | { 8 | public class HosterApiCallerTests 9 | { 10 | [Fact] 11 | public void GetRepositoryListCallsUnderlyingHosterApi() 12 | { 13 | var reader = new FakeConfigReader(); 14 | reader.SetDefaultFakeConfig(); 15 | var config = reader.ReadConfig(); 16 | var source = config.Sources.First(); 17 | 18 | var factory = new FakeHosterFactory(); 19 | var hoster = new FakeHoster(); 20 | factory.FakeHoster = hoster; 21 | 22 | var sut = new HosterApiCaller(factory); 23 | sut.GetRepositoryList(source); 24 | 25 | Assert.True(hoster.FakeApi.WasCalled); 26 | } 27 | 28 | [Fact] 29 | public void ThrowsWhenConfigSourceIsNull() 30 | { 31 | var factory = new FakeHosterFactory(); 32 | var hoster = new FakeHoster(); 33 | factory.FakeHoster = hoster; 34 | 35 | var sut = new HosterApiCaller(factory); 36 | Assert.Throws(() => sut.GetRepositoryList(null)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ConfigReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using YamlDotNet.Serialization; 3 | using YamlDotNet.Serialization.NamingConventions; 4 | 5 | namespace ScmBackup.Configuration 6 | { 7 | /// 8 | /// Reads the configuration values and returns an instance of the Config class 9 | /// 10 | internal class ConfigReader : IConfigReader 11 | { 12 | public string ConfigFileName { get; set; } 13 | 14 | private Config config = null; 15 | 16 | public ConfigReader() 17 | { 18 | this.ConfigFileName = "settings.yml"; 19 | } 20 | 21 | public Config ReadConfig() 22 | { 23 | if (this.config == null) 24 | { 25 | this.config = new Config(); 26 | 27 | var input = File.ReadAllText(this.ConfigFileName); 28 | var deserializer = new DeserializerBuilder() 29 | .WithNamingConvention(CamelCaseNamingConvention.Instance) 30 | .IgnoreUnmatchedProperties() 31 | .Build(); 32 | this.config = deserializer.Deserialize(input); 33 | } 34 | 35 | return this.config; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/LoggingHosterApiCallerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using ScmBackup.Tests.Hosters; 4 | using System.Collections.Generic; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests 8 | { 9 | public class LoggingHosterApiCallerTests 10 | { 11 | ConfigSource source; 12 | LoggingHosterApiCaller sut; 13 | FakeLogger logger; 14 | List repos; 15 | 16 | public LoggingHosterApiCallerTests() 17 | { 18 | this.source = new ConfigSource { Title = "foo" }; 19 | 20 | this.repos = new List(); 21 | this.repos.Add(new HosterRepository("foo.bar", "bar", "http://clone", ScmType.Git)); 22 | 23 | var caller = new FakeHosterApiCaller(); 24 | caller.Lists.Add(this.source, repos); 25 | 26 | this.logger = new FakeLogger(); 27 | 28 | this.sut = new LoggingHosterApiCaller(caller, logger); 29 | } 30 | 31 | [Fact] 32 | public void LogsInfoMessage() 33 | { 34 | this.sut.GetRepositoryList(this.source); 35 | 36 | Assert.True(this.logger.LoggedSomething); 37 | Assert.Equal(ErrorLevel.Info, this.logger.LastErrorLevel); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeFileSystemHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace ScmBackup.Tests 7 | { 8 | class FakeFileSystemHelper : IFileSystemHelper 9 | { 10 | public List SubDirectoryNames { get; set; } = new List(); 11 | 12 | public List DeletedDirectories { get; set; } = new List(); 13 | 14 | public void CreateDirectory(string path) 15 | { 16 | throw new NotImplementedException(); 17 | } 18 | 19 | public string CreateSubDirectory(string mainDir, string subDir) 20 | { 21 | throw new NotImplementedException(); 22 | } 23 | 24 | public bool DirectoryIsEmpty(string path) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public IEnumerable GetSubDirectoryNames(string path) 30 | { 31 | return this.SubDirectoryNames; 32 | } 33 | 34 | public string PathCombine(string path1, string path2) 35 | { 36 | return Path.Combine(path1, path2); 37 | } 38 | public void DeleteDirectory(string path) 39 | { 40 | this.DeletedDirectories.Add(path); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/HosterNameHelperTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using System; 3 | using Xunit; 4 | 5 | namespace ScmBackup.Tests.Hosters 6 | { 7 | public class HosterNameHelperTests 8 | { 9 | [Fact] 10 | public void ReturnsHosterName() 11 | { 12 | var t = typeof(FakeHoster); 13 | 14 | var result = HosterNameHelper.GetHosterName(t, "hoster"); 15 | 16 | Assert.Equal("fake", result); 17 | } 18 | 19 | [Fact] 20 | public void ThrowsWhenTypeNameDoesntEndWithSuffix() 21 | { 22 | var t = typeof(FakeHosterApi); 23 | 24 | Assert.Throws(() => HosterNameHelper.GetHosterName(t, "hoster")); 25 | } 26 | 27 | [Fact] 28 | public void ThrowsWhenTypeNameDoesntContainSuffix() 29 | { 30 | var t = typeof(FooBarBar); 31 | 32 | Assert.Throws(() => HosterNameHelper.GetHosterName(t, "hoster")); 33 | } 34 | 35 | [Fact] 36 | public void ThrowsWhenTypeNameContainsSuffixMoreThanOnce() 37 | { 38 | var t = typeof(FooBarBar); 39 | 40 | Assert.Throws(() => HosterNameHelper.GetHosterName(t, "bar")); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ContextTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace ScmBackup.Tests 4 | { 5 | public class ContextTests 6 | { 7 | [Fact] 8 | public void DoesNotThrowExceptions() 9 | { 10 | // most of the Context class is .NET Framework functionality (which we don't want to test again), 11 | // but at least we want to be noticed when anything throws an exception 12 | 13 | var reader = new FakeConfigReader(); 14 | reader.SetDefaultFakeConfig(); 15 | var sut = new Context(reader); 16 | 17 | var version = sut.VersionNumber; 18 | string versionString = sut.VersionNumberString; 19 | string appTitle = sut.AppTitle; 20 | } 21 | 22 | [Fact] 23 | public void UsesConfigFromReader() 24 | { 25 | var reader = new FakeConfigReader(); 26 | reader.SetDefaultFakeConfig(); 27 | var sut = new Context(reader); 28 | 29 | Assert.NotNull(sut.Config); 30 | 31 | // This checks reference equality (not content), but in this case it's good enough. 32 | // We just want to know whether the context returns the config that came from the IConfigReader. 33 | Assert.Equal(reader.FakeConfig, sut.Config); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyVersion("0.0.0")] 9 | [assembly: AssemblyFileVersion("0.0.0")] 10 | [assembly: AssemblyInformationalVersion("0.0.0-DEV")] 11 | [assembly: AssemblyTitle("ScmBackup.Tests.Integration")] 12 | [assembly: AssemblyDescription("Integration test library for SCM Backup")] 13 | [assembly: AssemblyConfiguration("")] 14 | [assembly: AssemblyCompany("scm-backup.org")] 15 | [assembly: AssemblyProduct("SCM Backup")] 16 | [assembly: AssemblyCopyright("Copyright © Christian Specht 2016")] 17 | [assembly: AssemblyTrademark("")] 18 | [assembly: AssemblyCulture("")] 19 | 20 | // Setting ComVisible to false makes the types in this assembly not visible 21 | // to COM components. If you need to access a type in this assembly from 22 | // COM, set the ComVisible attribute to true on that type. 23 | [assembly: ComVisible(false)] 24 | 25 | // The following GUID is for the ID of the typelib if this project is exposed to COM 26 | [assembly: Guid("6e03432d-9856-4699-bc7a-be60b615f720")] 27 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/EnvironmentVariableConfigReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace ScmBackup.Configuration 6 | { 7 | /// 8 | /// decorator for ConfigReader, replaces %foo% values with the respective environment variables 9 | /// 10 | internal class EnvironmentVariableConfigReader : IConfigReader 11 | { 12 | private readonly IConfigReader configReader; 13 | private Config config = null; 14 | 15 | public EnvironmentVariableConfigReader(IConfigReader configReader) 16 | { 17 | this.configReader = configReader; 18 | } 19 | 20 | public Config ReadConfig() 21 | { 22 | if (this.config != null) 23 | { 24 | return this.config; 25 | } 26 | 27 | var config = this.configReader.ReadConfig(); 28 | 29 | foreach (var source in config.Sources) 30 | { 31 | if (!string.IsNullOrWhiteSpace(source.Password)) 32 | { 33 | source.Password = Environment.ExpandEnvironmentVariables(source.Password); 34 | } 35 | } 36 | 37 | this.config = config; 38 | return config; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/TestLogger.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Loggers; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup.Tests.Integration 6 | { 7 | /// 8 | /// real logger for use in tests 9 | /// 10 | internal class TestLogger : ILogger 11 | { 12 | private readonly ILogger logger; 13 | 14 | public TestLogger(string logName) 15 | { 16 | // We are using the same logger like ScmBackup, but it's wrapped in this class. 17 | // So the real logger isn't hardcoded in lots of tests, should we ever change it. 18 | this.logger = new NLogLogger(); 19 | 20 | this.Log(ErrorLevel.Info, "STARTING: " + logName); 21 | } 22 | 23 | public void Log(ErrorLevel level, string message, params object[] arg) 24 | { 25 | this.logger.Log(level, message, arg); 26 | } 27 | 28 | public void Log(ErrorLevel level, Exception ex, string message, params object[] arg) 29 | { 30 | this.logger.Log(level, ex, message, arg); 31 | } 32 | 33 | public List FilesToBackup 34 | { 35 | get { return new List(); } 36 | } 37 | 38 | public void ExecuteOnExit(bool successful) 39 | { 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/HosterFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using ScmBackup.Hosters; 3 | using ScmBackup.Tests.Hosters; 4 | using SimpleInjector; 5 | using System; 6 | using Xunit; 7 | 8 | namespace ScmBackup.Tests.Integration 9 | { 10 | public class HosterFactoryTests 11 | { 12 | private readonly HosterFactory sut; 13 | 14 | public HosterFactoryTests() 15 | { 16 | sut = new HosterFactory(new Container()); 17 | sut.Register(typeof(FakeHoster)); 18 | } 19 | 20 | [Fact] 21 | public void NewHosterIsAdded() 22 | { 23 | Assert.Single(sut); 24 | } 25 | 26 | [Fact] 27 | public void CreateReturnsHoster() 28 | { 29 | var result = sut.Create("fake"); 30 | 31 | Assert.NotNull(result); 32 | Assert.True(result is IHoster); 33 | } 34 | 35 | [Fact] 36 | public void CreateThrowsWhenGivenNonExistingHoster() 37 | { 38 | Assert.ThrowsAny(() => sut.Create("foo")); 39 | } 40 | 41 | [Fact] 42 | public void RegisterThrowsIfRegisteredTypeIsNotIHoster() 43 | { 44 | Assert.Throws(() => sut.Register(typeof(ScmBackup))); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Http/UrlHelperTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Http; 2 | using Xunit; 3 | 4 | namespace ScmBackup.Tests.Http 5 | { 6 | public class UrlHelperTests 7 | { 8 | [Theory] 9 | [InlineData("http://scm-backup.org", true)] 10 | [InlineData("https://github.com", true)] 11 | [InlineData(null, false)] 12 | [InlineData("", false)] 13 | [InlineData("foo", false)] 14 | [InlineData("file:///c:/foo.txt", false)] 15 | public void ValidatesUrls(string url, bool expectedResult) 16 | { 17 | var sut = new UrlHelper(); 18 | Assert.Equal(expectedResult, sut.UrlIsValid(url)); 19 | } 20 | 21 | [Theory] 22 | [InlineData("http://user:pass@example.com/", "http://example.com/")] 23 | [InlineData("http://user@example.com/", "http://example.com/")] 24 | [InlineData("https://user:pass@example.com/", "https://example.com/")] 25 | [InlineData("https://user@example.com/", "https://example.com/")] 26 | [InlineData("https://example.com/", "https://example.com/")] 27 | public void RemovesCredentials(string oldUrl, string expectedUrl) 28 | { 29 | var sut = new UrlHelper(); 30 | Assert.Equal(expectedUrl, sut.RemoveCredentialsFromUrl(oldUrl)); 31 | 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyVersion("0.0.0")] 9 | [assembly: AssemblyFileVersion("0.0.0")] 10 | [assembly: AssemblyInformationalVersion("0.0.0-DEV")] 11 | [assembly: AssemblyTitle("ScmBackup.Tests")] 12 | [assembly: AssemblyDescription("Test library for SCM Backup")] 13 | [assembly: AssemblyConfiguration("")] 14 | [assembly: AssemblyCompany("scm-backup.org")] 15 | [assembly: AssemblyProduct("SCM Backup")] 16 | [assembly: AssemblyCopyright("Copyright © Christian Specht 2016")] 17 | [assembly: AssemblyTrademark("")] 18 | [assembly: AssemblyCulture("")] 19 | [assembly: InternalsVisibleTo("ScmBackup.Tests.Integration")] 20 | 21 | // Setting ComVisible to false makes the types in this assembly not visible 22 | // to COM components. If you need to access a type in this assembly from 23 | // COM, set the ComVisible attribute to true on that type. 24 | [assembly: ComVisible(false)] 25 | 26 | // The following GUID is for the ID of the typelib if this project is exposed to COM 27 | [assembly: Guid("52b54dbd-5a6f-4e5a-a0c7-b30bcf5a85f9")] 28 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/HosterNameHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ScmBackup.Hosters 4 | { 5 | public static class HosterNameHelper 6 | { 7 | /// 8 | /// Gets a hoster name from a type name via convention (the part before the suffix is the hoster name) 9 | /// 10 | public static string GetHosterName(Type type, string suffix) 11 | { 12 | var name = type.Name.ToLower(); 13 | suffix = suffix.ToLower(); 14 | 15 | int n = name.IndexOf(suffix); 16 | int n2 = name.LastIndexOf(suffix); 17 | 18 | if (n == 0) 19 | { 20 | throw new InvalidOperationException(string.Format(Resource.HosterNameError, type.Name, suffix) + Resource.HosterNameError_NoSuffix); 21 | } 22 | 23 | if (!name.EndsWith(suffix)) 24 | { 25 | throw new InvalidOperationException(string.Format(Resource.HosterNameError, type.Name, suffix)+ Resource.HosterNameError_End); 26 | } 27 | 28 | if (n != n2) 29 | { 30 | throw new InvalidOperationException(string.Format(Resource.HosterNameError, type.Name, suffix) + Resource.HosterNameError_MultiSuffix); 31 | } 32 | 33 | return name.Substring(0, n); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ScmBackup/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | #More info on customizing debug container. 3 | #https://learn.microsoft.com/en-us/visualstudio/containers/container-build?view=vs-2022#debugging 4 | 5 | ARG TZ 6 | FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base 7 | USER root 8 | 9 | # Set the time zone to Stockholm 10 | ENV TZ=Europe/Stockholm 11 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 12 | 13 | # Install Git 14 | RUN apt-get update && apt-get install -y git 15 | 16 | USER app 17 | WORKDIR /app 18 | 19 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build 20 | ARG BUILD_CONFIGURATION=Release 21 | WORKDIR /src 22 | COPY ["src/ScmBackup/ScmBackup.csproj", "src/ScmBackup/"] 23 | RUN dotnet restore "./src/ScmBackup/./ScmBackup.csproj" 24 | COPY . . 25 | WORKDIR "/src/src/ScmBackup" 26 | RUN dotnet build "./ScmBackup.csproj" -c $BUILD_CONFIGURATION -o /app/build 27 | 28 | FROM build AS publish 29 | ARG BUILD_CONFIGURATION=Release 30 | RUN dotnet publish "./ScmBackup.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false 31 | 32 | FROM base AS final 33 | WORKDIR /app 34 | COPY --from=publish /app/publish . 35 | 36 | ENTRYPOINT ["dotnet", "ScmBackup.dll"] -------------------------------------------------------------------------------- /src/ScmBackup/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyVersion("0.0.0")] 9 | [assembly: AssemblyFileVersion("0.0.0")] 10 | [assembly: AssemblyInformationalVersion("0.0.0-DEV")] 11 | [assembly: AssemblyTitle("SCM Backup")] 12 | [assembly: AssemblyDescription("Backup repositories from cloud hosters to your local machine or S3 bucket")] 13 | [assembly: AssemblyConfiguration("")] 14 | [assembly: AssemblyCompany("scm-backup.org")] 15 | [assembly: AssemblyProduct("SCM Backup")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyCulture("")] 18 | [assembly: InternalsVisibleTo("ScmBackup.Tests")] 19 | [assembly: InternalsVisibleTo("ScmBackup.Tests.Integration")] 20 | 21 | // Setting ComVisible to false makes the types in this assembly not visible 22 | // to COM components. If you need to access a type in this assembly from 23 | // COM, set the ComVisible attribute to true on that type. 24 | [assembly: ComVisible(false)] 25 | 26 | // The following GUID is for the ID of the typelib if this project is exposed to COM 27 | [assembly: Guid("430b8951-bc74-40f9-8a7d-d3fe5ca361e2")] 28 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/DirectoryHelperTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Xunit; 3 | 4 | namespace ScmBackup.Tests.Integration 5 | { 6 | public class DirectoryHelperTests 7 | { 8 | [Fact] 9 | public void DirectoryIsCreated() 10 | { 11 | var result = DirectoryHelper.CreateTempDirectory(); 12 | 13 | Assert.False(string.IsNullOrWhiteSpace(result)); 14 | Assert.True(Directory.Exists(result)); 15 | } 16 | 17 | [Fact] 18 | public void DirectoryWithSuffixIsCreated() 19 | { 20 | string suffix = "foo"; 21 | 22 | var result = DirectoryHelper.CreateTempDirectory(suffix); 23 | 24 | Assert.False(string.IsNullOrWhiteSpace(result)); 25 | Assert.True(Directory.Exists(result)); 26 | Assert.EndsWith(suffix, result); 27 | } 28 | 29 | [Fact] 30 | public void TestAssemblyDirectoryWorks() 31 | { 32 | // Difficult to test, because it's hard to determine the path *without* using the method under test. 33 | // -> at least make sure it doesn't throw and it's a real path 34 | string result = DirectoryHelper.TestAssemblyDirectory(); 35 | 36 | Assert.False(string.IsNullOrWhiteSpace(result), result); 37 | Assert.True(Directory.Exists(result), result); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ScmBackup/Loggers/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ScmBackup.Loggers 5 | { 6 | /// 7 | /// Logs to the console 8 | /// 9 | internal class ConsoleLogger : ILogger 10 | { 11 | public void Log(ErrorLevel level, string message, params object[] arg) 12 | { 13 | this.Log(level, null, message, arg); 14 | } 15 | 16 | public void Log(ErrorLevel level, Exception ex, string message, params object[] arg) 17 | { 18 | switch (level) 19 | { 20 | case ErrorLevel.Debug: 21 | return; 22 | case ErrorLevel.Warn: 23 | Console.ForegroundColor = ConsoleColor.Yellow; 24 | break; 25 | case ErrorLevel.Error: 26 | Console.ForegroundColor = ConsoleColor.Red; 27 | break; 28 | } 29 | 30 | if (ex != null) 31 | { 32 | message += " " + ex.Message; 33 | } 34 | 35 | Console.WriteLine(message, arg); 36 | Console.ResetColor(); 37 | } 38 | 39 | public List FilesToBackup 40 | { 41 | get { return null; } 42 | } 43 | 44 | public void ExecuteOnExit(bool successful) 45 | { 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ScmBackup/CompositionRoot/ScmFactory.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using SimpleInjector; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | 7 | namespace ScmBackup.CompositionRoot 8 | { 9 | /// 10 | /// factory to create IScm instances 11 | /// 12 | internal class ScmFactory : Dictionary, IScmFactory 13 | { 14 | private readonly Container container; 15 | 16 | public ScmFactory(Container container) 17 | { 18 | this.container = container; 19 | } 20 | 21 | public void Register(Type type) 22 | { 23 | if (!typeof(IScm).IsAssignableFrom(type)) 24 | { 25 | throw new InvalidOperationException(string.Format(Resource.TypeIsNoIScm, type.ToString())); 26 | } 27 | 28 | var attribute = type.GetTypeInfo().GetCustomAttribute(); 29 | 30 | this.container.Register(type); 31 | this.Add(attribute.Type, type); 32 | } 33 | 34 | public IScm Create(ScmType type) 35 | { 36 | Type outType; 37 | 38 | if (!this.TryGetValue(type, out outType)) 39 | { 40 | throw new InvalidOperationException(string.Format(Resource.ScmDoesntExist, type)); 41 | } 42 | 43 | return (IScm)this.container.GetInstance(outType); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ScmBackup/Scm/CommandLineResult.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Scm 2 | { 3 | /// 4 | /// return value for CommandLineScm 5 | /// 6 | internal class CommandLineResult 7 | { 8 | public CommandLineResult() 9 | { 10 | this.ExitCode = int.MinValue; 11 | } 12 | 13 | /// 14 | /// The error output of the command 15 | /// 16 | public string StandardError { get; set; } 17 | 18 | /// 19 | /// The standard output of the command 20 | /// 21 | public string StandardOutput { get; set; } 22 | 23 | /// 24 | /// The exit code of the command 25 | /// 26 | public int ExitCode { get; set; } 27 | 28 | /// 29 | /// The output (standard or error, whichever was set) of the command 30 | /// 31 | public string Output 32 | { 33 | get 34 | { 35 | return string.IsNullOrWhiteSpace(this.StandardError) ? this.StandardOutput : this.StandardError; 36 | } 37 | } 38 | 39 | /// 40 | /// Did the command execute successfully? 41 | /// 42 | public bool Successful 43 | { 44 | get 45 | { 46 | return (this.ExitCode == 0); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ScmBackup/CompositionRoot/HosterFactory.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using SimpleInjector; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | 7 | namespace ScmBackup.CompositionRoot 8 | { 9 | /// 10 | /// factory which creates IHoster instances 11 | /// 12 | internal class HosterFactory : Dictionary, IHosterFactory 13 | { 14 | private readonly Container container; 15 | 16 | public HosterFactory(Container container) 17 | { 18 | this.container = container; 19 | } 20 | 21 | public void Register(Type type) 22 | { 23 | if (!typeof(IHoster).IsAssignableFrom(type)) 24 | { 25 | throw new InvalidOperationException(string.Format(Resource.TypeIsNoIHoster, type.ToString())); 26 | } 27 | 28 | string hosterName = HosterNameHelper.GetHosterName(type, "hoster"); 29 | 30 | this.container.Register(type); 31 | this.Add(hosterName, type); 32 | } 33 | 34 | public IHoster Create(string hosterName) 35 | { 36 | Type type; 37 | 38 | if (!this.TryGetValue(hosterName, out type)) 39 | { 40 | throw new InvalidOperationException(string.Format(Resource.HosterDoesntExist, hosterName)); 41 | } 42 | 43 | return (IHoster)this.container.GetInstance(type); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /stack-ad/ad.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageFont, ImageDraw, ImageEnhance 2 | 3 | 4 | # define fonts 5 | font_logo = ImageFont.truetype('calibrib.ttf', 64) 6 | font_cont = ImageFont.truetype('calibrib.ttf', 40) 7 | font = ImageFont.truetype('calibri.ttf', 30) 8 | 9 | 10 | # empty image 11 | img = Image.new('RGBA', (600,500), 'white') 12 | dr = ImageDraw.Draw(img) 13 | 14 | 15 | # logo 16 | logo = Image.open('../img/logo128x128.png').convert("RGBA") 17 | img.paste(logo, (35,35), logo) 18 | 19 | 20 | # logo text 21 | dr.text((220,75), 'SCM Backup', font=font_logo, fill='black') 22 | 23 | 24 | # blue box 25 | dr.rectangle(((0, 190), (600, 270)), fill='#239FE6') 26 | dr.text((75,200), 'Makes offline backups of your cloud', font=font, fill='white') 27 | dr.text((75,230), 'hosted source code repositories', font=font, fill='white') 28 | 29 | 30 | # "Contribute" text 31 | dr.text((75,310), 'Help us implement support (in C#) for', font=font, fill='black') 32 | dr.text((75,340), 'backing up from more hosting sites!', font=font, fill='black') 33 | dr.text((50,430), 'Contribute on GitHub', font=font_cont, fill='black') 34 | 35 | 36 | # GitHub logo (download from https://github.com/logos, put into this directory) 37 | ghlogo = Image.open('GitHub-Mark-64px.png') 38 | img.paste(ghlogo, (450,420), ghlogo) 39 | 40 | 41 | 42 | # 2px black border 43 | dr.rectangle(((0, 0), (599, 499)), outline='black') 44 | dr.rectangle(((1, 1), (598, 498)), outline='black') 45 | 46 | 47 | img.save('ad.png') 48 | 49 | 50 | -------------------------------------------------------------------------------- /.github/workflows/ci-linux.yml: -------------------------------------------------------------------------------- 1 | name: "Linux build" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Setup .NET 13 | uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: 8.0.x 16 | - name: Set version number 17 | run: .\version-number.ps1 18 | shell: pwsh 19 | - name: Run build script 20 | env: 21 | Tests_Github_Name : ${{ secrets.Tests_Github_Name }} 22 | Tests_Github_PW : ${{ secrets.Tests_Github_PW }} 23 | Tests_Github_RepoPrivate : ${{ secrets.Tests_Github_RepoPrivate }} 24 | Tests_Bitbucket_Name : ${{ secrets.Tests_Bitbucket_Name }} 25 | Tests_Bitbucket_PW : ${{ secrets.Tests_Bitbucket_PW }} 26 | Tests_Bitbucket_RepoPrivateGit : ${{ secrets.Tests_Bitbucket_RepoPrivateGit }} 27 | Tests_Gitlab_Name : ${{ secrets.Tests_Gitlab_Name }} 28 | Tests_Gitlab_PW : ${{ secrets.Tests_Gitlab_PW }} 29 | Tests_Gitlab_RepoPrivate : ${{ secrets.Tests_Gitlab_RepoPrivate }} 30 | run: ./build-release.ps1 31 | shell: pwsh 32 | - name: Upload application 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: Application 36 | path: release/scm-backup-*.zip 37 | - name: Upload test log 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: Test log 41 | path: src/ScmBackup.Tests.Integration/bin/Release/net8.0/*.log 42 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/HosterBackupMakerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using ScmBackup.Hosters; 3 | using System; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Hosters 8 | { 9 | public class HosterBackupMakerTests 10 | { 11 | [Fact] 12 | public void MakeBackupCallsUnderlyingMethod() 13 | { 14 | var hoster = new FakeHoster(); 15 | 16 | var factory = new FakeHosterFactory(hoster); 17 | var repo = new HosterRepository("foo", "foo", "http://clone", ScmType.Git); 18 | repo.SetWiki(true, "http://wiki"); 19 | repo.SetIssues(true, "http://issues"); 20 | 21 | var reader = new FakeConfigReader(); 22 | reader.SetDefaultFakeConfig(); 23 | var config = reader.ReadConfig(); 24 | var source = config.Sources.First(); 25 | 26 | var sut = new HosterBackupMaker(factory); 27 | sut.MakeBackup(source, repo, "foo"); 28 | 29 | Assert.True(hoster.FakeBackup.WasExecuted); 30 | } 31 | 32 | [Fact] 33 | public void ThrowsWhenConfigSourceIsNull() 34 | { 35 | var factory = new FakeHosterFactory(new FakeHoster()); 36 | var repo = new HosterRepository("foo", "foo", "http://clone", ScmType.Git); 37 | 38 | var sut = new HosterBackupMaker(factory); 39 | Assert.Throws(() => sut.MakeBackup(null, repo, "foo")); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ScmBackup/Loggers/NLogLogger.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup.Loggers 6 | { 7 | /// 8 | /// Logs to NLog 9 | /// 10 | internal class NLogLogger : ILogger 11 | { 12 | private Logger logger = LogManager.GetLogger("ScmBackup"); 13 | 14 | public void Log(ErrorLevel level, string message, params object[] arg) 15 | { 16 | this.Log(level, null, message, arg); 17 | } 18 | 19 | public void Log(ErrorLevel level, Exception ex, string message, params object[] arg) 20 | { 21 | LogLevel NLogLevel = LogLevel.Trace; 22 | 23 | switch (level) 24 | { 25 | case ErrorLevel.Debug: 26 | NLogLevel = LogLevel.Debug; 27 | break; 28 | case ErrorLevel.Error: 29 | NLogLevel = LogLevel.Error; 30 | break; 31 | case ErrorLevel.Info: 32 | NLogLevel = LogLevel.Info; 33 | break; 34 | case ErrorLevel.Warn: 35 | NLogLevel = LogLevel.Warn; 36 | break; 37 | } 38 | 39 | this.logger.Log(NLogLevel, ex, message, arg); 40 | } 41 | 42 | public List FilesToBackup 43 | { 44 | get { return new List { "NLog.config" }; } 45 | } 46 | 47 | public void ExecuteOnExit(bool successful) 48 | { 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Configuration/EnvironmentVariableConfigReaderTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Xunit; 7 | 8 | namespace ScmBackup.Tests.Configuration 9 | { 10 | public class EnvironmentVariableConfigReaderTests 11 | { 12 | private IConfigReader sut; 13 | private FakeConfigReader reader; 14 | 15 | public EnvironmentVariableConfigReaderTests() 16 | { 17 | reader = new FakeConfigReader(); 18 | reader.SetDefaultFakeConfig(); 19 | sut = new EnvironmentVariableConfigReader(reader); 20 | 21 | Environment.SetEnvironmentVariable("scmbackup_test", "foo"); 22 | } 23 | 24 | [Theory] 25 | [InlineData("%scmbackup_test%bar", "foobar")] // part of the password 26 | [InlineData("%scmbackup_test%", "foo")] // whole password 27 | public void ReplacesInPassword(string originalPw, string changedPw) 28 | { 29 | reader.FakeConfig.Sources.First().Password = originalPw; 30 | 31 | var result = sut.ReadConfig(); 32 | 33 | Assert.Equal(changedPw, result.Sources.First().Password); 34 | } 35 | 36 | [Fact] 37 | public void DoesNothingWhenPasswordContainsNoVariable() 38 | { 39 | reader.FakeConfig.Sources.First().Password = "bar"; 40 | 41 | var result = sut.ReadConfig(); 42 | 43 | Assert.Equal("bar", result.Sources.First().Password); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/IgnoringHosterApiCallerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using ScmBackup.Tests.Hosters; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Xunit; 7 | 8 | namespace ScmBackup.Tests 9 | { 10 | public class IgnoringHosterApiCallerTests 11 | { 12 | ConfigSource source; 13 | IgnoringHosterApiCaller sut; 14 | 15 | public IgnoringHosterApiCallerTests() 16 | { 17 | this.source = new ConfigSource { Title = "foo" }; 18 | 19 | var repos = new List(); 20 | repos.Add(new HosterRepository("foo.bar", "bar", "http://clone", ScmType.Git)); 21 | repos.Add(new HosterRepository("foo.baz", "baz", "http://clone", ScmType.Git)); 22 | 23 | var caller = new FakeHosterApiCaller(); 24 | caller.Lists.Add(this.source, repos); 25 | 26 | this.sut = new IgnoringHosterApiCaller(caller); 27 | } 28 | 29 | [Fact] 30 | public void IgnoresRepo() 31 | { 32 | this.source.IgnoreRepos = new List { "bar" }; 33 | 34 | var list = this.sut.GetRepositoryList(this.source); 35 | 36 | Assert.Single(list); 37 | Assert.Equal("baz", list.First().ShortName); 38 | } 39 | 40 | [Fact] 41 | public void DoesntIgnoreWhenNotSet() 42 | { 43 | // don't set this.source.IgnoreRepos 44 | 45 | var list = this.sut.GetRepositoryList(this.source); 46 | 47 | Assert.Equal(2, list.Count); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/FakeHoster.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup.Tests.Hosters 6 | { 7 | internal class FakeHoster : IHoster 8 | { 9 | public FakeHoster() 10 | { 11 | this.Validator = new FakeConfigSourceValidator(); 12 | this.FakeValidator.Result = new ValidationResult(); 13 | 14 | this.Api = new FakeHosterApi(); 15 | this.FakeApi.RepoList = new List(); 16 | 17 | this.Backup = new FakeHosterBackup(); 18 | } 19 | 20 | /// 21 | /// easier access (without casting) to the fake validator 22 | /// 23 | public FakeConfigSourceValidator FakeValidator 24 | { 25 | get { return (FakeConfigSourceValidator)this.Validator; } 26 | } 27 | 28 | /// 29 | /// easier access (without casting) to the fake API 30 | /// 31 | public FakeHosterApi FakeApi 32 | { 33 | get { return (FakeHosterApi)this.Api; } 34 | } 35 | 36 | /// 37 | /// easier access (without casting) to the fake backupper 38 | /// 39 | public FakeHosterBackup FakeBackup 40 | { 41 | get { return (FakeHosterBackup)this.Backup; } 42 | } 43 | 44 | public IConfigSourceValidator Validator { get; private set; } 45 | public IHosterApi Api { get; private set; } 46 | public IBackup Backup { get; private set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ScmBackup/Context.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | using System.Reflection; 4 | 5 | namespace ScmBackup 6 | { 7 | /// 8 | /// "application context" for global information 9 | /// 10 | internal class Context : IContext 11 | { 12 | private readonly IConfigReader reader; 13 | private Config config; 14 | 15 | public Context(IConfigReader reader) 16 | { 17 | this.reader = reader; 18 | 19 | var assembly = typeof(ScmBackup).GetTypeInfo().Assembly; 20 | this.VersionNumber = assembly.GetName().Version; 21 | this.VersionNumberString= assembly.GetCustomAttribute().InformationalVersion; 22 | this.AppTitle = Resource.AppTitle + " " + this.VersionNumberString; 23 | this.UserAgent = Resource.AppTitle.Replace(" ", "-"); 24 | } 25 | 26 | public Version VersionNumber { get; private set; } 27 | 28 | public string VersionNumberString { get; private set; } 29 | 30 | public string AppTitle { get; private set; } 31 | 32 | public string UserAgent { get; private set; } 33 | 34 | public Config Config 35 | { 36 | get 37 | { 38 | if (this.config == null) 39 | { 40 | this.Config = this.reader.ReadConfig(); 41 | } 42 | 43 | return this.config; 44 | } 45 | private set 46 | { 47 | this.config = value; 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /version-number.ps1: -------------------------------------------------------------------------------- 1 | if ($env:APPVEYOR) { 2 | # we are on AppVeyor 3 | 4 | $commit = $env:APPVEYOR_REPO_COMMIT.Substring(0,7) 5 | 6 | if ($env:APPVEYOR_REPO_TAG -eq $true) { 7 | # we are building a tag -> we are doing a release -> use the tag as version number 8 | $shortversion = $env:APPVEYOR_REPO_TAG_NAME 9 | $longversion = $shortversion + '.' + $commit 10 | } 11 | else { 12 | # regular CI build, no release 13 | $shortversion = '0.0.0' 14 | $longversion = '0.0.0.CI-WIN-' + $env:APPVEYOR_BUILD_NUMBER + '-' + $commit 15 | } 16 | } 17 | elseif ($env:GITHUB_ACTIONS) { 18 | 19 | # GH Actions 20 | $commit = $env:GITHUB_SHA.Substring(0,7) 21 | $shortversion = '0.0.0' 22 | $longversion = '0.0.0.CI-LINUX-' + $env:GITHUB_RUN_NUMBER + '-' + $commit 23 | } 24 | else { 25 | 26 | # local build 27 | 28 | Set-Location $PSScriptRoot 29 | $commit = git rev-parse --short HEAD 30 | 31 | $shortversion = '0.0.0' 32 | $longversion = '0.0.0.DEV-' + $commit 33 | } 34 | 35 | $env:ScmBackupCommit=$commit 36 | $env:ScmBackupShortVersion=$shortversion 37 | $env:ScmBackupLongVersion=$longversion 38 | 39 | Write-Host 'Commit: ' $env:ScmBackupCommit 40 | Write-Host 'Short Version: ' $env:ScmBackupShortVersion 41 | Write-Host 'Long Version: ' $env:ScmBackupLongVersion 42 | 43 | if ($env:GITHUB_ACTIONS) { 44 | # for GH Actions, save version numbers so they are available in subsequent steps 45 | "ScmBackupCommit=$commit" >> $env:GITHUB_ENV 46 | "ScmBackupShortVersion=$shortversion" >> $env:GITHUB_ENV 47 | "ScmBackupLongVersion=$longversion" >> $env:GITHUB_ENV 48 | } -------------------------------------------------------------------------------- /src/ScmBackup.Tests/IncludingHosterApiCallerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using ScmBackup.Tests.Hosters; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using Xunit; 9 | 10 | namespace ScmBackup.Tests 11 | { 12 | public class IncludingHosterApiCallerTests 13 | { 14 | ConfigSource source; 15 | IncludingHosterApiCaller sut; 16 | 17 | public IncludingHosterApiCallerTests() 18 | { 19 | this.source = new ConfigSource { Title = "foo" }; 20 | 21 | var repos = new List(); 22 | repos.Add(new HosterRepository("foo.bar", "bar", "http://clone", ScmType.Git)); 23 | repos.Add(new HosterRepository("foo.baz", "baz", "http://clone", ScmType.Git)); 24 | 25 | var caller = new FakeHosterApiCaller(); 26 | caller.Lists.Add(this.source, repos); 27 | 28 | this.sut = new IncludingHosterApiCaller(caller); 29 | } 30 | 31 | [Fact] 32 | public void IncludesRepo() 33 | { 34 | this.source.IncludeRepos = new List { "bar" }; 35 | 36 | var list = this.sut.GetRepositoryList(this.source); 37 | 38 | Assert.Single(list); 39 | Assert.Equal("bar", list.First().ShortName); 40 | } 41 | 42 | [Fact] 43 | public void BackupsEverythingWhenNotSet() 44 | { 45 | // don't set this.source.IncludeRepos 46 | 47 | var list = this.sut.GetRepositoryList(this.source); 48 | 49 | Assert.Equal(2, list.Count); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/HttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace ScmBackup.Http 8 | { 9 | /// 10 | /// Wrapper for HttpClient 11 | /// 12 | internal class HttpRequest : IHttpRequest 13 | { 14 | public HttpClient HttpClient { get; set; } 15 | 16 | public HttpRequest() 17 | { 18 | this.HttpClient = new HttpClient(); 19 | } 20 | 21 | public void SetBaseUrl(string url) 22 | { 23 | this.HttpClient.BaseAddress = new Uri(url); 24 | } 25 | 26 | public void AddHeader(string name, string value) 27 | { 28 | this.HttpClient.DefaultRequestHeaders.Add(name, value); 29 | } 30 | 31 | public void AddBasicAuthHeader(string username, string password) 32 | { 33 | var byteArray = Encoding.ASCII.GetBytes(username + ":" + password); 34 | this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); 35 | } 36 | 37 | public async Task Execute(string url) 38 | { 39 | var result = new HttpResult(); 40 | var response = await this.HttpClient.GetAsync(url); 41 | result.Status = response.StatusCode; 42 | result.IsSuccessStatusCode = response.IsSuccessStatusCode; 43 | result.Headers = response.Headers; 44 | result.Content = await response.Content.ReadAsStringAsync(); 45 | 46 | return result; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Hosters/HosterRepositoryTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using Xunit; 3 | 4 | namespace ScmBackup.Tests.Hosters 5 | { 6 | public class HosterRepositoryTests 7 | { 8 | [Theory] 9 | [InlineData("foobar", "foobar")] 10 | [InlineData("foo/bar", "foo#bar")] 11 | public void InvalidCharsInRepoNameAreReplaced(string inputName, string savedName) 12 | { 13 | var sut = new HosterRepository(inputName, "name", "http://clone", ScmType.Git); 14 | Assert.Equal(savedName, sut.FullName); 15 | 16 | sut = new HosterRepository(inputName, "name", "http://clone", ScmType.Git, false, "", false, ""); 17 | Assert.Equal(savedName, sut.FullName); 18 | } 19 | 20 | [Fact] 21 | public void SetWikiWorks() 22 | { 23 | var sut = new HosterRepository("foo", "foo", "http://clone", ScmType.Git); 24 | sut.SetWiki(true, "url"); 25 | 26 | Assert.True(sut.HasWiki); 27 | Assert.Equal("url", sut.WikiUrl); 28 | } 29 | 30 | [Fact] 31 | public void SetIssuesWorks() 32 | { 33 | var sut = new HosterRepository("foo", "foo", "http://clone", ScmType.Git); 34 | sut.SetIssues(true, "url"); 35 | 36 | Assert.True(sut.HasIssues); 37 | Assert.Equal("url", sut.IssueUrl); 38 | } 39 | 40 | [Fact] 41 | public void IsPrivateWorks() 42 | { 43 | var sut = new HosterRepository("foo", "foo", "http://clone", ScmType.Git); 44 | sut.SetPrivate(true); 45 | 46 | Assert.True(sut.IsPrivate); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/AddTimestampedSubfolderConfigReader .cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace ScmBackup.Configuration 7 | { 8 | /// 9 | /// decorator for ConfigReader, append timestamped subfolder to backup folder 10 | /// 11 | internal class AddTimestampedSubfolderConfigReader : IConfigReader 12 | { 13 | private readonly IConfigReader configReader; 14 | private readonly IFileSystemHelper fileHelper; 15 | private Config config = null; 16 | 17 | public AddTimestampedSubfolderConfigReader(IConfigReader configReader, IFileSystemHelper fileHelper) 18 | { 19 | this.configReader = configReader; 20 | this.fileHelper = fileHelper; 21 | } 22 | 23 | public Config ReadConfig() 24 | { 25 | if (this.config != null) 26 | { 27 | return this.config; 28 | } 29 | 30 | var config = this.configReader.ReadConfig(); 31 | 32 | if (config.Options.Backup.AddTimestampedSubfolder) { 33 | string localFolder = config.LocalFolder; 34 | string timestampFormat = config.Options.Backup.TimestampFormat; 35 | string timestamp = DateTime.Now.ToString(timestampFormat); 36 | this.fileHelper.CreateSubDirectory(localFolder, timestamp); 37 | config.LocalFolder = Path.Combine(localFolder, timestamp); 38 | } 39 | 40 | this.config = config; 41 | return config; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ScmBackup/settings.yml: -------------------------------------------------------------------------------- 1 | ########################################################################### 2 | ## 3 | ## SCM Backup 4 | ## https://scm-backup.org/ 5 | ## Makes offline backups of your cloud hosted source code repositories 6 | ## 7 | ## Documentation about this config file: 8 | ## https://docs.scm-backup.org/en/latest/config.html 9 | ## 10 | ########################################################################### 11 | 12 | 13 | # all backups go here 14 | localFolder: 'c:\scm-backup' 15 | 16 | # all backups go here on docker 17 | # localFolder: '/app/backups' 18 | 19 | # when an error occurs, wait that many seconds before exiting the application 20 | waitSecondsOnError: 5 21 | 22 | # uncomment this to send SCM Backup's console output via email 23 | #email: 24 | # from: from@example.com 25 | # to: to@example.com 26 | # server: smtp.example.com 27 | # port: 0 28 | # useSsl: false 29 | # userName: testuser 30 | # password: not-the-real-password 31 | 32 | sources: 33 | 34 | - title: github_singleuser 35 | hoster: github 36 | type: user 37 | name: scm-backup-testuser 38 | 39 | 40 | options: 41 | backup: 42 | # delete repos from local backup that don't exist at the hoster 43 | removeDeletedRepos : false 44 | 45 | # output a second log message for each repo when backing it up is finished 46 | logRepoFinished: false 47 | 48 | # add subfolder to localfolder in format yyyy-MM-dd_HHmm 49 | addTimestampedSubfolder: false 50 | 51 | # timestamp format for timestamped subfolder. 52 | timestampFormat: yyyy-MM-dd_HHmm 53 | 54 | # if this is not empty, save the files to an S3 bucket 55 | s3BucketName: 56 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ConfigBackupMaker.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace ScmBackup.Configuration 5 | { 6 | internal class ConfigBackupMaker : IConfigBackupMaker 7 | { 8 | private readonly IContext context; 9 | private readonly ILogger logger; 10 | 11 | public ConfigBackupMaker(IContext context, ILogger logger) 12 | { 13 | this.context = context; 14 | this.logger = logger; 15 | } 16 | 17 | /// 18 | /// subfolder where the configs are saved 19 | /// 20 | public string SubFolder 21 | { 22 | get { return "_config"; } 23 | } 24 | 25 | /// 26 | /// File names of the config files to backup 27 | /// 28 | public List ConfigFileNames 29 | { 30 | get 31 | { 32 | var list = this.logger.FilesToBackup; 33 | list.Add("settings.yml"); 34 | return list; 35 | } 36 | } 37 | 38 | /// 39 | /// Copies important config files into the backup folder. 40 | /// 41 | public void BackupConfigs() 42 | { 43 | string backupDir = Path.Combine(this.context.Config.LocalFolder, this.SubFolder); 44 | Directory.CreateDirectory(backupDir); 45 | 46 | foreach (var file in this.ConfigFileNames) 47 | { 48 | File.Copy(file, Path.Combine(backupDir, file), true); 49 | } 50 | 51 | this.logger.Log(ErrorLevel.Info, Resource.BackingUpConfigs); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ScmBackup/Loggers/CompositeLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ScmBackup.Loggers 6 | { 7 | /// 8 | /// Wrapper, calls the other loggers 9 | /// 10 | internal class CompositeLogger : ILogger 11 | { 12 | private IEnumerable loggers; 13 | 14 | public CompositeLogger(IEnumerable loggers) 15 | { 16 | this.loggers = loggers; 17 | } 18 | 19 | public void Log(ErrorLevel level, string message, params object[] arg) 20 | { 21 | this.Log(level, null, message, arg); 22 | } 23 | 24 | public void Log(ErrorLevel level, Exception ex, string message, params object[] arg) 25 | { 26 | foreach (var logger in this.loggers) 27 | { 28 | logger.Log(level, ex, message, arg); 29 | } 30 | } 31 | 32 | public List FilesToBackup 33 | { 34 | get 35 | { 36 | var list = new List(); 37 | foreach (var logger in this.loggers) 38 | { 39 | if (logger.FilesToBackup != null && logger.FilesToBackup.Any()) 40 | { 41 | list.AddRange(logger.FilesToBackup); 42 | } 43 | } 44 | 45 | return list; 46 | } 47 | } 48 | 49 | public void ExecuteOnExit(bool successful) 50 | { 51 | foreach (var logger in this.loggers) 52 | { 53 | logger.ExecuteOnExit(successful); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ScmBackup/ApiRepositories.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace ScmBackup 8 | { 9 | internal class ApiRepositories 10 | { 11 | public Dictionary> Dic { get; private set; } 12 | 13 | public ApiRepositories() 14 | { 15 | this.Dic = new Dictionary>(); 16 | } 17 | 18 | public void AddItem(ConfigSource config, List repos) 19 | { 20 | this.Dic.Add(config, repos); 21 | } 22 | 23 | public IEnumerable GetSources() 24 | { 25 | return this.Dic.Keys.ToList(); 26 | } 27 | 28 | public IEnumerable GetReposForSource(ConfigSource config) 29 | { 30 | return this.Dic[config].OrderBy(r => r.FullName); 31 | } 32 | 33 | /// 34 | /// Returns a unique list of all ScmTypes from all HosterRepositories 35 | /// 36 | public HashSet GetScmTypes() 37 | { 38 | if (!this.Dic.Any()) 39 | { 40 | throw new InvalidOperationException(Resource.ApiRepositoriesContainsNoHosterRepos); 41 | } 42 | 43 | var result = new HashSet(); 44 | 45 | foreach (var item in this.Dic) 46 | { 47 | foreach (var repo in item.Value) 48 | { 49 | result.Add(repo.Scm); 50 | } 51 | } 52 | 53 | return result; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/LoggingHttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace ScmBackup.Http 5 | { 6 | /// 7 | /// logging decorator for HttpRequest 8 | /// 9 | internal class LoggingHttpRequest : IHttpRequest 10 | { 11 | private readonly IHttpRequest request; 12 | private readonly ILogger logger; 13 | 14 | public LoggingHttpRequest(IHttpRequest request, ILogger logger) 15 | { 16 | this.request = request; 17 | this.logger = logger; 18 | } 19 | 20 | public HttpClient HttpClient 21 | { 22 | get { return this.request.HttpClient; } 23 | set { this.request.HttpClient = value; } 24 | } 25 | 26 | public void AddHeader(string name, string value) 27 | { 28 | this.request.AddHeader(name, value); 29 | } 30 | 31 | public void AddBasicAuthHeader(string username, string password) 32 | { 33 | this.request.AddBasicAuthHeader(username, password); 34 | } 35 | 36 | public async Task Execute(string url) 37 | { 38 | string className = this.GetType().Name; 39 | 40 | this.logger.Log(ErrorLevel.Debug, Resource.HttpRequest, url); 41 | var result = await this.request.Execute(url); 42 | 43 | this.logger.Log(ErrorLevel.Debug, Resource.HttpHeaders, result.Headers.ToString()); 44 | this.logger.Log(ErrorLevel.Debug, Resource.HttpResult, result.Content); 45 | 46 | return result; 47 | } 48 | 49 | public void SetBaseUrl(string url) 50 | { 51 | this.request.SetBaseUrl(url); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/DirectoryHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Reflection; 5 | 6 | namespace ScmBackup.Tests.Integration 7 | { 8 | /// 9 | /// helper class to create unique temp directories for integration tests 10 | /// 11 | public class DirectoryHelper 12 | { 13 | public static string CreateTempDirectory() 14 | { 15 | return DirectoryHelper.CreateTempDirectory(string.Empty); 16 | } 17 | 18 | public static string CreateTempDirectory(string suffix) 19 | { 20 | string tempDir = Path.GetTempPath(); 21 | string subDir = "_scm-backup-tests"; 22 | string newDir = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture); 23 | 24 | if (!string.IsNullOrWhiteSpace(suffix)) 25 | { 26 | newDir += '-' + suffix; 27 | } 28 | 29 | string finalDir = Path.Combine(tempDir, subDir, newDir); 30 | 31 | if (Directory.CreateDirectory(finalDir) != null) 32 | { 33 | return finalDir; 34 | } 35 | 36 | return string.Empty; 37 | } 38 | 39 | /// 40 | /// Returns the directory of the current test assembly (usually bin/debug) 41 | /// 42 | public static string TestAssemblyDirectory() 43 | { 44 | string unc = typeof(DirectoryHelper).GetTypeInfo().Assembly.Location; 45 | 46 | // convert from UNC path to "real" path 47 | var uri = new Uri(unc); 48 | string file = uri.LocalPath; 49 | 50 | return Path.GetDirectoryName(file); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ScmBackup/Scm/ScmValidator.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace ScmBackup.Scm 6 | { 7 | /// 8 | /// Verifies that all passed SCMs are present on this machine 9 | /// 10 | internal class ScmValidator : IScmValidator 11 | { 12 | private readonly IScmFactory factory; 13 | private readonly ILogger logger; 14 | 15 | public ScmValidator(IScmFactory factory, ILogger logger) 16 | { 17 | this.factory = factory; 18 | this.logger = logger; 19 | } 20 | 21 | public bool ValidateScms(HashSet scms) 22 | { 23 | bool ok = true; 24 | this.logger.Log(ErrorLevel.Info, Resource.ScmValidatorStarting); 25 | 26 | foreach (var scmType in scms) 27 | { 28 | var scm = this.factory.Create(scmType); 29 | 30 | bool onComputer = false; 31 | try 32 | { 33 | onComputer = scm.IsOnThisComputer(); 34 | } 35 | catch (Exception ex) 36 | { 37 | this.logger.Log(ErrorLevel.Error, ex, scm.DisplayName + ": "); 38 | } 39 | 40 | if (onComputer) 41 | { 42 | this.logger.Log(ErrorLevel.Info, Resource.ScmOnThisComputer, scm.DisplayName + " " + scm.GetVersionNumber()); 43 | } 44 | else 45 | { 46 | this.logger.Log(ErrorLevel.Error, Resource.ScmNotOnThisComputer, scm.DisplayName); 47 | ok = false; 48 | } 49 | } 50 | 51 | return ok; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ScmBackup/ScmBackup.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Scm; 3 | using System; 4 | 5 | namespace ScmBackup 6 | { 7 | /// 8 | /// main program execution 9 | /// 10 | internal class ScmBackup : IScmBackup 11 | { 12 | private readonly IApiCaller apiCaller; 13 | private readonly IScmValidator validator; 14 | private readonly IBackupMaker backupMaker; 15 | private readonly IConfigBackupMaker configBackupMaker; 16 | private readonly IDeletedRepoHandler deletedHandler; 17 | 18 | public ScmBackup(IApiCaller apiCaller, IScmValidator validator, IBackupMaker backupMaker, IConfigBackupMaker configBackupMaker, IDeletedRepoHandler deletedHandler) 19 | { 20 | this.apiCaller = apiCaller; 21 | this.validator = validator; 22 | this.backupMaker = backupMaker; 23 | this.configBackupMaker = configBackupMaker; 24 | this.deletedHandler = deletedHandler; 25 | } 26 | 27 | public bool Run() 28 | { 29 | this.configBackupMaker.BackupConfigs(); 30 | 31 | var repos = this.apiCaller.CallApis(); 32 | 33 | if (!this.validator.ValidateScms(repos.GetScmTypes())) 34 | { 35 | throw new InvalidOperationException(Resource.ScmValidatorError); 36 | } 37 | 38 | foreach (var source in repos.GetSources()) 39 | { 40 | var sourceRepos = repos.GetReposForSource(source); 41 | string sourceFolder = this.backupMaker.Backup(source, sourceRepos); 42 | this.deletedHandler.HandleDeletedRepos(sourceRepos, sourceFolder); 43 | } 44 | 45 | return true; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Hosters/GitlabApiTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters.Gitlab; 2 | using ScmBackup.Http; 3 | using ScmBackup.Scm; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace ScmBackup.Tests.Integration.Hosters 9 | { 10 | public class GitlabApiTests : IHosterApiTests 11 | { 12 | internal override string HosterUser { get { return "scm-backup-testuser"; } } 13 | 14 | internal override string HosterOrganization { get { return "scm-backup-testgroup"; } } 15 | 16 | internal override string HosterRepo { get { return "scm-backup-test"; } } 17 | 18 | internal override string HosterCommit { get { return "d7c9ad8185b7707dbcc907e41154e3e5e5b2a540"; } } 19 | 20 | internal override string HosterWikiCommit { get { return "5893873f9da26fc59bbeaafde5fad5800907e56f"; } } 21 | 22 | internal override string HosterPaginationUser { get { return "dzaporozhets"; } } 23 | 24 | internal override string HosterPrivateRepo { get { return TestHelper.EnvVar(this.EnvVarPrefix, "RepoPrivate"); } } 25 | 26 | internal override string EnvVarPrefix { get { return "Tests_Gitlab"; } } 27 | 28 | internal override string ConfigHoster { get { return "gitlab"; } } 29 | 30 | internal override int Pagination_MinNumberOfRepos { get { return 20; } } // https://docs.gitlab.com/ee/api/README.html#pagination 31 | 32 | internal override bool SkipUnauthenticatedTests { get { return false; } } 33 | 34 | public GitlabApiTests() 35 | { 36 | var context = new FakeContext(); 37 | var factory = new FakeScmFactory(); 38 | factory.Register(ScmType.Git, new GitScm(new FileSystemHelper(), context)); 39 | 40 | this.sut = new GitlabApi(new HttpRequest(), factory); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/ConfigBackupMakerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System.IO; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace ScmBackup.Tests.Integration 7 | { 8 | public class ConfigBackupMakerTests 9 | { 10 | [Fact] 11 | public void SubfolderIsSet() 12 | { 13 | var sut = new ConfigBackupMaker(new FakeContext(), new FakeLogger()); 14 | Assert.True(!string.IsNullOrWhiteSpace(sut.SubFolder)); 15 | } 16 | 17 | [Fact] 18 | public void ConfigFileNamesAreSet() 19 | { 20 | var sut = new ConfigBackupMaker(new FakeContext(), new FakeLogger()); 21 | Assert.True(sut.ConfigFileNames.Any()); 22 | } 23 | 24 | [Fact] 25 | public void CopiesAllFiles() 26 | { 27 | var config = new Config(); 28 | config.LocalFolder = DirectoryHelper.CreateTempDirectory("configbackupmaker1"); 29 | 30 | var context = new FakeContext(); 31 | context.Config = config; 32 | 33 | var sut = new ConfigBackupMaker(context, new FakeLogger()); 34 | sut.BackupConfigs(); 35 | 36 | foreach (var file in sut.ConfigFileNames) 37 | { 38 | string path = Path.Combine(config.LocalFolder, sut.SubFolder, file); 39 | 40 | Assert.True(File.Exists(path), file); 41 | } 42 | } 43 | 44 | [Fact] 45 | public void ExecutesMultipleTimes() 46 | { 47 | var config = new Config(); 48 | config.LocalFolder = DirectoryHelper.CreateTempDirectory("configbackupmaker2"); 49 | 50 | var context = new FakeContext(); 51 | context.Config = config; 52 | 53 | var sut = new ConfigBackupMaker(context, new FakeLogger()); 54 | sut.BackupConfigs(); 55 | sut.BackupConfigs(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ScmBackup/Loggers/EmailLogger.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Http; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace ScmBackup.Loggers 7 | { 8 | /// 9 | /// Sends log via email 10 | /// 11 | class EmailLogger : ILogger 12 | { 13 | private readonly IEmailSender mail; 14 | private List messages; 15 | 16 | public EmailLogger(IEmailSender mail) 17 | { 18 | this.mail = mail; 19 | this.messages = new List(); 20 | } 21 | public List FilesToBackup 22 | { 23 | get { return null; } 24 | } 25 | 26 | public void ExecuteOnExit(bool successful) 27 | { 28 | string success = successful ? Resource.LogMailSubjectSuccess : Resource.LogMailSubjectFailed; 29 | 30 | string subject = string.Format(Resource.LogMailSubject, success, DateTime.Now.ToString("dd MMM HH:mm:ss")); 31 | string body = string.Join(Environment.NewLine, this.messages); 32 | 33 | this.mail.Send(subject, body); 34 | } 35 | 36 | public void Log(ErrorLevel level, string message, params object[] arg) 37 | { 38 | this.Log(level, null, message, arg); 39 | } 40 | 41 | public void Log(ErrorLevel level, Exception ex, string message, params object[] arg) 42 | { 43 | if (level == ErrorLevel.Debug) 44 | { 45 | return; 46 | } 47 | 48 | var tmp = new StringBuilder(); 49 | tmp.Append(level.LevelName()); 50 | 51 | if (ex != null) 52 | { 53 | tmp.Append(" "); 54 | tmp.Append(ex.Message); 55 | } 56 | 57 | tmp.Append(" "); 58 | tmp.AppendFormat(message, arg); 59 | 60 | this.messages.Add(tmp.ToString()); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ScmBackup/DeletedRepoHandler.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace ScmBackup 8 | { 9 | internal class DeletedRepoHandler : IDeletedRepoHandler 10 | { 11 | private readonly ILogger logger; 12 | private readonly IFileSystemHelper fileHelper; 13 | private readonly IContext context; 14 | 15 | public DeletedRepoHandler(ILogger logger, IFileSystemHelper fileHelper, IContext context) 16 | { 17 | this.logger = logger; 18 | this.fileHelper = fileHelper; 19 | this.context = context; 20 | } 21 | 22 | public IEnumerable HandleDeletedRepos(IEnumerable repos, string backupDir) 23 | { 24 | var alldirs = this.fileHelper.GetSubDirectoryNames(backupDir); 25 | var repodirs = repos.Select(x => x.FullName); 26 | 27 | var deletedRepoDirs = alldirs.Except(repodirs); 28 | 29 | if (deletedRepoDirs.Any()) 30 | { 31 | bool remove = this.context.Config.Options.Backup.RemoveDeletedRepos; 32 | 33 | if (remove) 34 | { 35 | this.logger.Log(ErrorLevel.Warn, Resource.DeletedRepoRemoving); 36 | } 37 | else 38 | { 39 | this.logger.Log(ErrorLevel.Warn, Resource.DeletedRepoWarning); 40 | } 41 | 42 | foreach (string dir in deletedRepoDirs) 43 | { 44 | if (remove) 45 | { 46 | this.fileHelper.DeleteDirectory(this.fileHelper.PathCombine(backupDir, dir)); 47 | } 48 | 49 | this.logger.Log(ErrorLevel.Warn, " " + dir); 50 | } 51 | } 52 | 53 | return deletedRepoDirs; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ScmBackup.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | ScmBackup.Tests 6 | ScmBackup.Tests 7 | true 8 | false 9 | false 10 | false 11 | false 12 | false 13 | false 14 | false 15 | false 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ValidationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace ScmBackup.Configuration 5 | { 6 | /// 7 | /// return value for validators 8 | /// 9 | internal class ValidationResult 10 | { 11 | private readonly ConfigSource source; 12 | 13 | public ValidationResult() : this(null) 14 | { 15 | } 16 | 17 | public ValidationResult(ConfigSource configSource) 18 | { 19 | this.Messages = new List(); 20 | this.source = configSource; 21 | } 22 | 23 | public bool IsValid 24 | { 25 | get 26 | { 27 | return !this.Messages.Any(m => m.Error == ErrorLevel.Error); 28 | } 29 | } 30 | 31 | public List Messages { get; private set; } 32 | 33 | public void AddMessage(ErrorLevel error, string message) 34 | { 35 | this.AddMessage(error, message, ValidationMessageType.Undefined); 36 | } 37 | 38 | public void AddMessage(ErrorLevel error, string message, ValidationMessageType type) 39 | { 40 | if (this.source != null) 41 | { 42 | message = this.source.Title + ": " + message; 43 | } 44 | 45 | this.Messages.Add(new ValidationMessage(error, message, type)); 46 | } 47 | 48 | internal class ValidationMessage 49 | { 50 | 51 | public ValidationMessage(ErrorLevel error, string message, ValidationMessageType type) 52 | { 53 | this.Error = error; 54 | this.Message = message; 55 | this.Type = type; 56 | } 57 | 58 | public ErrorLevel Error { get; private set; } 59 | public string Message { get; private set; } 60 | public ValidationMessageType Type { get; private set; } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/BootstrapperTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.CompositionRoot; 2 | using ScmBackup.Hosters.Bitbucket; 3 | using ScmBackup.Hosters.Github; 4 | using System; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Integration 8 | { 9 | public class BootstrapperTests 10 | { 11 | /// 12 | /// Just initialize the container like on startup. 13 | /// If a dependency could not be resolved, it will throw an exception. 14 | /// 15 | [Fact] 16 | public void DependenciesWereResolved() 17 | { 18 | bool error = false; 19 | 20 | try 21 | { 22 | var container = Bootstrapper.BuildContainer(); 23 | } 24 | catch (InvalidOperationException) 25 | { 26 | error = true; 27 | } 28 | 29 | Assert.False(error); 30 | } 31 | 32 | /// 33 | /// Make sure the name-based convention for the hosters and their subclasses works correctly: 34 | /// Resolve from container and check whether *Github*Hoster actually contains *Github*Api, and so on. 35 | /// 36 | [Fact] 37 | public void HosterAutoRegistrationConventionWorks() 38 | { 39 | var container = Bootstrapper.BuildContainer(); 40 | 41 | var factory = container.GetInstance(); 42 | 43 | var gh = factory.Create("github"); 44 | Assert.Equal(typeof(GithubHoster), gh.GetType()); 45 | Assert.Equal(typeof(GithubApi), gh.Api.GetType()); 46 | Assert.Equal(typeof(GithubConfigSourceValidator), gh.Validator.GetType()); 47 | 48 | var bb = factory.Create("bitbucket"); 49 | Assert.Equal(typeof(BitbucketHoster), bb.GetType()); 50 | Assert.Equal(typeof(BitbucketApi), bb.Api.GetType()); 51 | Assert.Equal(typeof(BitbucketConfigSourceValidator), bb.Validator.GetType()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Loggers/CompositeLoggerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Loggers; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | 5 | namespace ScmBackup.Tests.Loggers 6 | { 7 | public class CompositeLoggerTests 8 | { 9 | [Fact] 10 | public void CallsAllUnderlyingLoggers() 11 | { 12 | var loggers = new List { new FakeLogger(), new FakeLogger() }; 13 | var sut = new CompositeLogger(loggers); 14 | 15 | sut.Log(ErrorLevel.Info, "foo"); 16 | 17 | foreach(var mock in loggers) 18 | { 19 | Assert.True(mock.LoggedSomething); 20 | Assert.Equal(ErrorLevel.Info, mock.LastErrorLevel); 21 | Assert.Equal("foo", mock.LastMessage); 22 | } 23 | 24 | } 25 | 26 | [Fact] 27 | public void ExecutesAllUnderlyingLoggers() 28 | { 29 | var logger1 = new FakeLogger(); 30 | var logger2 = new FakeLogger(); 31 | 32 | var loggers = new List { logger1, logger2 }; 33 | var sut = new CompositeLogger(loggers); 34 | sut.ExecuteOnExit(true); 35 | 36 | Assert.True(logger1.ExecutedOnExit && logger2.ExecutedOnExit); 37 | } 38 | 39 | [Fact] 40 | public void ReturnsFilesFromAllUnderlyingLoggers() 41 | { 42 | var logger1 = new FakeLogger(); 43 | logger1.FakeFilesToBackup = new List { "a.txt", "b.txt" }; 44 | 45 | var logger2 = new FakeLogger(); 46 | logger2.FakeFilesToBackup = new List { "c.txt" }; 47 | 48 | var loggers = new List { logger1, logger2 }; 49 | var sut = new CompositeLogger(loggers); 50 | var list = sut.FilesToBackup; 51 | 52 | Assert.Equal(3, list.Count); 53 | Assert.Contains("a.txt", list); 54 | Assert.Contains("b.txt", list); 55 | Assert.Contains("c.txt", list); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ScmBackup/Http/MailKitEmailSender.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Net.Smtp; 2 | using MimeKit; 3 | 4 | namespace ScmBackup.Http 5 | { 6 | internal class MailKitEmailSender : IEmailSender 7 | { 8 | private readonly IContext context; 9 | 10 | public MailKitEmailSender(IContext context) 11 | { 12 | this.context = context; 13 | } 14 | 15 | public void Send(string subject, string body) 16 | { 17 | var config = this.context.Config.Email; 18 | 19 | if (config == null) 20 | { 21 | return; 22 | } 23 | 24 | var message = new MimeMessage(); 25 | message.From.Add(new MailboxAddress("SCM Backup", config.From)); 26 | foreach (var to in config.To_AsList()) 27 | { 28 | message.To.Add(new MailboxAddress("", to)); 29 | } 30 | message.Subject = subject; 31 | 32 | message.Body = new TextPart("plain") 33 | { 34 | Text = body 35 | }; 36 | 37 | using (var client = new SmtpClient()) 38 | { 39 | // TODO: copied from MailKit's docs, not sure if we need this 40 | // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS) 41 | client.ServerCertificateValidationCallback = (s, c, h, e) => true; 42 | 43 | client.Connect(config.Server, config.Port, config.UseSsl); 44 | 45 | if (client.Capabilities.HasFlag(SmtpCapabilities.Authentication)) 46 | { 47 | if (!string.IsNullOrWhiteSpace(config.UserName) && !string.IsNullOrWhiteSpace(config.Password)) 48 | { 49 | client.Authenticate(config.UserName, config.Password); 50 | } 51 | } 52 | 53 | client.Send(message); 54 | client.Disconnect(true); 55 | } 56 | 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Scm/CommandLineScmTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System; 3 | using System.IO; 4 | using Xunit; 5 | 6 | namespace ScmBackup.Tests.Integration.Scm 7 | { 8 | public class CommandLineScmTests 9 | { 10 | [Fact] 11 | public void ReallyExecutes() 12 | { 13 | var sut = new FakeCommandLineScm(); 14 | var result = sut.IsOnThisComputer(); 15 | 16 | Assert.True(result); 17 | } 18 | 19 | [Fact] 20 | public void ThrowsWhenPathFromConfigDoesntExist() 21 | { 22 | var sut = new FakeCommandLineScm(); 23 | 24 | var config = new Config(); 25 | config.Scms.Add(new ConfigScm { Name = sut.ShortName, Path = sut.FakeCommandNameNotExisting }); 26 | 27 | sut.Context = FakeContext.BuildFakeContextWithConfig(config); 28 | 29 | Assert.Throws(() => sut.IsOnThisComputer()); 30 | } 31 | 32 | [Fact] 33 | public void ReallyExecutesWithPathFromConfig() 34 | { 35 | var sut = new FakeCommandLineScm(); 36 | 37 | var config = new Config(); 38 | config.Scms.Add(new ConfigScm { Name = sut.ShortName, Path = sut.FakeCommandName }); 39 | 40 | sut.Context = FakeContext.BuildFakeContextWithConfig(config); 41 | 42 | var result = sut.IsOnThisComputer(); 43 | 44 | Assert.True(result); 45 | } 46 | 47 | [Fact] 48 | public void ExecuteReturnsOutput() 49 | { 50 | var sut = new FakeCommandLineScm(); 51 | 52 | var result = sut.ExecuteCommandDirectly(); 53 | 54 | Assert.Contains(sut.FakeCommandResult, result); 55 | } 56 | 57 | [Fact] 58 | public void ThrowsWhenContextIsNull() 59 | { 60 | var sut = new FakeCommandLineScm(); 61 | sut.Context = null; 62 | 63 | Assert.Throws(() => sut.IsOnThisComputer()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/DeletedRepoHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Hosters; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Xunit; 7 | 8 | namespace ScmBackup.Tests 9 | { 10 | public class DeletedRepoHandlerTests 11 | { 12 | FakeLogger logger; 13 | FakeFileSystemHelper fhelper; 14 | FakeContext context; 15 | List repos; 16 | 17 | public DeletedRepoHandlerTests() 18 | { 19 | this.logger = new FakeLogger(); 20 | this.fhelper = new FakeFileSystemHelper(); 21 | this.context = new FakeContext(); 22 | 23 | this.repos = new List 24 | { 25 | new HosterRepository("repo1","repo1","url", ScmType.Git), 26 | new HosterRepository("repo2","repo2","url", ScmType.Git) 27 | }; 28 | 29 | this.fhelper.SubDirectoryNames = new List { "repo1", "repo2", "missing1", "missing2" }; 30 | } 31 | 32 | [Fact] 33 | public void DetectsDeletedDirs() 34 | { 35 | var sut = new DeletedRepoHandler(this.logger, this.fhelper, this.context); 36 | var result = sut.HandleDeletedRepos(repos, "dir"); 37 | 38 | Assert.Contains("missing1", result); 39 | Assert.Contains("missing2", result); 40 | } 41 | 42 | [Fact] 43 | public void RemovesDeletedDirs() 44 | { 45 | this.context.Config.Options.Backup.RemoveDeletedRepos = true; 46 | 47 | var sut = new DeletedRepoHandler(this.logger, this.fhelper, this.context); 48 | var result = sut.HandleDeletedRepos(repos, "dir"); 49 | 50 | var missing1 = fhelper.DeletedDirectories.Where(x => x.Contains("missing1")).FirstOrDefault(); 51 | Assert.NotNull(missing1); 52 | 53 | var missing2 = fhelper.DeletedDirectories.Where(x => x.Contains("missing2")).FirstOrDefault(); 54 | Assert.NotNull(missing2); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Scm/ScmValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using ScmBackup.Tests.Scm; 3 | using System; 4 | using System.Collections.Generic; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests 8 | { 9 | public class ScmValidatorTests 10 | { 11 | private FakeScmFactory factory; 12 | private FakeLogger logger; 13 | 14 | private HashSet scmlist; 15 | private FakeScm scm; 16 | 17 | private IScmValidator sut; 18 | 19 | public ScmValidatorTests() 20 | { 21 | this.logger = new FakeLogger(); 22 | 23 | this.scmlist = new HashSet(); 24 | this.scmlist.Add(ScmType.Git); 25 | 26 | this.scm = new FakeScm(); 27 | 28 | this.factory = new FakeScmFactory(); 29 | this.factory.Register(ScmType.Git, this.scm); 30 | 31 | this.sut = new ScmValidator(this.factory, this.logger); 32 | } 33 | 34 | [Fact] 35 | public void ReturnsTrueWhenScmsValidate() 36 | { 37 | this.scm.IsOnThisComputerResult = true; 38 | 39 | var result = this.sut.ValidateScms(this.scmlist); 40 | 41 | Assert.True(result); 42 | Assert.NotEqual(ErrorLevel.Error, this.logger.LastErrorLevel); 43 | } 44 | 45 | [Fact] 46 | public void ReturnsFalseWhenScmsDontValidate() 47 | { 48 | this.scm.IsOnThisComputerResult = false; 49 | 50 | var result = this.sut.ValidateScms(this.scmlist); 51 | 52 | Assert.False(result); 53 | Assert.Equal(ErrorLevel.Error, this.logger.LastErrorLevel); 54 | } 55 | 56 | [Fact] 57 | public void DoesNotThrowExceptionAndReturnsFalseWhenScmThrowsException() 58 | { 59 | this.scm.IsOnThisComputerException = new Exception("boom"); 60 | 61 | var result = this.sut.ValidateScms(this.scmlist); 62 | 63 | Assert.False(result); 64 | Assert.Equal(ErrorLevel.Error, this.logger.LastErrorLevel); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Scm/MercurialScmTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Http; 2 | using ScmBackup.Scm; 3 | using ScmBackup.Tests.Hosters; 4 | using System; 5 | 6 | namespace ScmBackup.Tests.Integration.Scm 7 | { 8 | // Test class disabled (=private) because none of the currently supported hosters supports Mercurial anymore. 9 | // If a Mercurial hoster is supported in the future, we need new test repos 10 | class MercurialScmTests : IScmTests 11 | { 12 | public MercurialScmTests() 13 | { 14 | this.sut = new MercurialScm(new FileSystemHelper(), new FakeContext(), new UrlHelper()); 15 | } 16 | 17 | internal override string PublicRepoUrl 18 | { 19 | get { return CloneUrlBuilder.BitbucketCloneUrl("scm-backup-testuser", "scm-backup-test"); } 20 | } 21 | 22 | internal override string PrivateRepoUrl 23 | { 24 | get { return CloneUrlBuilder.BitbucketCloneUrl(TestHelper.EnvVar("Tests_Bitbucket_Name"), TestHelper.EnvVar("Tests_Bitbucket_RepoPrivate")); } 25 | } 26 | 27 | internal override ScmCredentials PrivateRepoCredentials 28 | { 29 | get { return new ScmCredentials(TestHelper.EnvVar("Tests_Bitbucket_Name"), TestHelper.EnvVar("Tests_Bitbucket_PW")); } 30 | } 31 | 32 | internal override string NonExistingRepoUrl 33 | { 34 | get { return CloneUrlBuilder.BitbucketCloneUrl("scm-backup-testuser", "repo-does-not-exist"); } 35 | } 36 | 37 | internal override string DotRepoUrl 38 | { 39 | get { return null; } 40 | } 41 | 42 | internal override string PublicRepoExistingCommitId 43 | { 44 | get { return "617f9e55262be7b6d1c9db081ec351ff25c9a0e5"; } 45 | } 46 | 47 | internal override string PublicRepoNonExistingCommitId 48 | { 49 | get 50 | { 51 | // note: in Mercurial, commit id "000000000000" is valid, so we need to check for something which doesn't contain "000..." 52 | return "1111111111"; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/TestHelperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace ScmBackup.Tests 5 | { 6 | public class TestHelperTests 7 | { 8 | [Fact] 9 | public void EnvVar_ReturnsExistingVariable() 10 | { 11 | Environment.SetEnvironmentVariable("ThisVariableExists", "foo"); 12 | 13 | Assert.Equal("foo", TestHelper.EnvVar("ThisVariableExists")); 14 | } 15 | 16 | [Fact] 17 | public void EnvVar_ThrowsWhenRequestedVariableDoesNotExist() 18 | { 19 | Assert.Throws(() => TestHelper.EnvVar("ThisVariableDoesNotExist")); 20 | } 21 | 22 | [Fact] 23 | public void EnvVar_DoesNotThrowsWhenRequestedVariableDoesNotExist() 24 | { 25 | Assert.Null(TestHelper.EnvVar("ThisVariableDoesNotExist", false)); 26 | } 27 | 28 | [Fact] 29 | public void EnvVar_WithPrefix_ReturnsExistingVariable() 30 | { 31 | Environment.SetEnvironmentVariable("prefix_name", "foo"); 32 | 33 | Assert.Equal("foo", TestHelper.EnvVar("prefix", "name")); 34 | } 35 | 36 | [Fact] 37 | public void BuildRepositoryName_BuildsName() 38 | { 39 | Assert.Equal("user#repo", TestHelper.BuildRepositoryName("user", "repo")); 40 | } 41 | 42 | [Theory] 43 | [InlineData("user", null)] 44 | [InlineData("user", "")] 45 | [InlineData("user", " ")] 46 | [InlineData(null, "repo")] 47 | [InlineData("", "repo")] 48 | [InlineData(" ", "repo")] 49 | [InlineData("", "")] 50 | public void BuildRepositoryName_ThrowsWhenParameterIsEmpty(string userName, string repoName) 51 | { 52 | Assert.Throws(() => TestHelper.BuildRepositoryName(userName, repoName)); 53 | } 54 | 55 | [Fact] 56 | public void RunsOnAppVeyor_Works() 57 | { 58 | // We can't test the *result* without duplicating the "check whether we are on AppVeyor" 59 | // logic, but we can at least check whether the method executes without errors. 60 | bool result = TestHelper.RunsOnAppVeyor(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ScmBackup/ErrorHandlingScmBackup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace ScmBackup 5 | { 6 | /// 7 | /// decorator for ScmBackup, handles and logs errors 8 | /// 9 | internal class ErrorHandlingScmBackup : IScmBackup 10 | { 11 | private readonly IScmBackup backup; 12 | private readonly ILogger logger; 13 | private readonly IContext context; 14 | 15 | /// 16 | /// default wait time after an error occurs 17 | /// (overridable in the tests) 18 | /// 19 | public int WaitSecondsOnError { get; set; } 20 | 21 | public ErrorHandlingScmBackup(IScmBackup backup, ILogger logger, IContext context) 22 | { 23 | this.backup = backup; 24 | this.logger = logger; 25 | this.context = context; 26 | 27 | this.WaitSecondsOnError = 5; 28 | } 29 | 30 | public bool Run() 31 | { 32 | string className = this.GetType().Name; 33 | bool ok; 34 | 35 | try 36 | { 37 | this.logger.Log(ErrorLevel.Debug, Resource.StartingBackup, className); 38 | ok = this.backup.Run(); 39 | } 40 | catch (Exception ex) 41 | { 42 | this.logger.Log(ErrorLevel.Error, ex.Message); 43 | 44 | // Wait as many seconds as defined in the config. 45 | // If we don't have the config value because the exception was thrown while reading the config, use the default value defined in this class 46 | int seconds = this.WaitSecondsOnError; 47 | 48 | if (this.context.Config != null) 49 | { 50 | seconds = this.context.Config.WaitSecondsOnError; 51 | } 52 | 53 | this.logger.Log(ErrorLevel.Error, Resource.BackupFailed); 54 | this.logger.Log(ErrorLevel.Error, Resource.EndSeconds, seconds); 55 | Task.Delay(TimeSpan.FromSeconds(seconds)).Wait(); 56 | 57 | ok = false; 58 | } 59 | 60 | this.logger.ExecuteOnExit(ok); 61 | return ok; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/ConfigReaderTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using Xunit; 3 | using YamlDotNet.Core; 4 | 5 | namespace ScmBackup.Tests.Integration 6 | { 7 | public class ConfigReaderTests 8 | { 9 | [Fact] 10 | public void ReadsTestConfigFile() 11 | { 12 | var sut = new ConfigReader(); 13 | sut.ConfigFileName = "testsettings.yml"; 14 | 15 | var config = sut.ReadConfig(); 16 | 17 | Assert.Equal("localfolder", config.LocalFolder); 18 | Assert.Equal(5, config.WaitSecondsOnError); 19 | Assert.Equal(2, config.Sources.Count); 20 | Assert.Equal(2, config.Scms.Count); 21 | 22 | var source0 = config.Sources[0]; 23 | Assert.Equal("hoster0", source0.Hoster); 24 | Assert.Equal("type0", source0.Type); 25 | Assert.Equal("name0", source0.Name); 26 | 27 | var source1 = config.Sources[1]; 28 | Assert.Equal("hoster1", source1.Hoster); 29 | Assert.Equal("type1", source1.Type); 30 | Assert.Equal("name1", source1.Name); 31 | 32 | var ignores = source1.IgnoreRepos; 33 | Assert.Equal(2, ignores.Count); 34 | Assert.Equal("ignore0", ignores[0]); 35 | Assert.Equal("ignore1", ignores[1]); 36 | 37 | var scm1 = config.Scms[0]; 38 | Assert.Equal("git", scm1.Name); 39 | Assert.Equal("path/to/git", scm1.Path); 40 | 41 | var scm2 = config.Scms[1]; 42 | Assert.Equal("hg", scm2.Name); 43 | Assert.Equal("path/to/hg", scm2.Path); 44 | } 45 | 46 | [Fact] 47 | public void ThrowsExceptionWhenConfigFileIsNotVaild() 48 | { 49 | var sut = new ConfigReader(); 50 | sut.ConfigFileName = "brokensettings.yml"; 51 | 52 | Assert.ThrowsAny(() => sut.ReadConfig()); 53 | } 54 | 55 | [Fact] 56 | public void IgnoresUnknownProperties_Issue76() 57 | { 58 | var sut = new ConfigReader(); 59 | sut.ConfigFileName = "settings-unknown-property.yml"; 60 | 61 | var config = sut.ReadConfig(); // should just ignore the unknown property and NOT throw an exception 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/FakeLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ScmBackup.Tests 5 | { 6 | internal class FakeLogger : ILogger 7 | { 8 | public bool LoggedSomething { get; set; } 9 | public ErrorLevel LastErrorLevel { get; set; } 10 | public Exception LastException { get; set; } 11 | public string LastMessage { get; set; } 12 | public object[] LastArg { get; set; } 13 | public bool ExecutedOnExit { get; set; } 14 | public bool ExecuteOnExit_Successful { get; set; } 15 | 16 | public bool IgnoreDebugLogs { get; set; } 17 | public bool ConsoleOutput { get; set; } 18 | public List FakeFilesToBackup { get; set; } 19 | 20 | public FakeLogger() 21 | { 22 | // default setting: ignore all debug logs, because they "get in the way" 23 | // when checking whether the last log was an error, for example 24 | this.IgnoreDebugLogs = true; 25 | 26 | // default setting: don't actually output log messages 27 | this.ConsoleOutput = false; 28 | 29 | this.FakeFilesToBackup = new List(); 30 | } 31 | 32 | public void Log(ErrorLevel level, string message, params object[] arg) 33 | { 34 | this.Log(level, null, message, arg); 35 | } 36 | 37 | public void Log(ErrorLevel level, Exception ex, string message, params object[] arg) 38 | { 39 | if (level == ErrorLevel.Debug && this.IgnoreDebugLogs) 40 | { 41 | return; 42 | } 43 | 44 | if (this.ConsoleOutput) 45 | { 46 | Console.WriteLine(string.Format("[{0}] ", level) + string.Format(message, arg)); 47 | } 48 | 49 | this.LoggedSomething = true; 50 | this.LastErrorLevel = level; 51 | this.LastException = ex; 52 | this.LastMessage = message; 53 | this.LastArg = arg; 54 | } 55 | 56 | public List FilesToBackup 57 | { 58 | get { return this.FakeFilesToBackup; } 59 | } 60 | 61 | public void ExecuteOnExit(bool successful) 62 | { 63 | this.ExecutedOnExit = true; 64 | this.ExecuteOnExit_Successful = successful; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/ScmBackup/Scm/IScm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ScmBackup.Scm 4 | { 5 | internal interface IScm 6 | { 7 | /// 8 | /// Short name of the SCM (used to find settings in the config) 9 | /// 10 | string ShortName { get; } 11 | 12 | /// 13 | /// "Pretty" name for displaying 14 | /// 15 | string DisplayName { get; } 16 | 17 | /// 18 | /// Checks whether the SCM is present on this computer 19 | /// 20 | bool IsOnThisComputer(); 21 | 22 | /// 23 | /// Gets the SCM's version number. 24 | /// Should throw exceptions if the version number can't be determined. 25 | /// 26 | string GetVersionNumber(); 27 | 28 | /// 29 | /// Checks whether the given directory is a repository 30 | /// 31 | bool DirectoryIsRepository(string directory); 32 | 33 | /// 34 | /// Creates a repository in the given directory 35 | /// 36 | void CreateRepository(string directory); 37 | 38 | /// 39 | /// Checks whether a repository exists under the given URL 40 | /// 41 | bool RemoteRepositoryExists(string remoteUrl); 42 | 43 | /// 44 | /// Checks whether a repository exists under the given URL 45 | /// 46 | bool RemoteRepositoryExists(string remoteUrl, ScmCredentials credentials); 47 | 48 | /// 49 | /// Pulls from a remote repository into a local folder. 50 | /// If the folder doesn't exist or is not a repository, it's created first. 51 | /// 52 | void PullFromRemote(string remoteUrl, string directory); 53 | 54 | /// 55 | /// Pulls from a remote repository into a local folder. 56 | /// If the folder doesn't exist or is not a repository, it's created first. 57 | /// 58 | void PullFromRemote(string remoteUrl, string directory, ScmCredentials credentials); 59 | 60 | /// 61 | /// Checks whether the repo in this directory contains a commit with this ID 62 | /// 63 | bool RepositoryContainsCommit(string directory, string commitid); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Scm/FakeScm.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Scm; 3 | using System; 4 | 5 | namespace ScmBackup.Tests.Scm 6 | { 7 | internal class FakeScm : IScm 8 | { 9 | /// 10 | /// Value returned by IsOnThisComputer() 11 | /// 12 | public bool IsOnThisComputerResult { get; set; } 13 | 14 | /// 15 | /// Exception to be thrown by IsOnThisComputer 16 | /// 17 | public Exception IsOnThisComputerException { get; set; } 18 | 19 | public string ShortName 20 | { 21 | get { return "fake"; } 22 | } 23 | 24 | public string DisplayName 25 | { 26 | get { return "Fake"; } 27 | } 28 | 29 | public bool IsOnThisComputer() 30 | { 31 | return this.IsOnThisComputer(null); 32 | } 33 | 34 | public bool IsOnThisComputer(Config config) 35 | { 36 | if (this.IsOnThisComputerException != null) 37 | { 38 | throw this.IsOnThisComputerException; 39 | } 40 | 41 | return this.IsOnThisComputerResult; 42 | } 43 | 44 | public string GetVersionNumber() 45 | { 46 | return "fake"; 47 | } 48 | 49 | public bool DirectoryIsRepository(string directory) 50 | { 51 | throw new NotImplementedException(); 52 | } 53 | 54 | public void CreateRepository(string directory) 55 | { 56 | throw new NotImplementedException(); 57 | } 58 | 59 | public bool RemoteRepositoryExists(string remoteUrl) 60 | { 61 | throw new NotImplementedException(); 62 | } 63 | 64 | public bool RemoteRepositoryExists(string remoteUrl, ScmCredentials credentials) 65 | { 66 | throw new NotImplementedException(); 67 | } 68 | 69 | public void PullFromRemote(string remoteUrl, string directory) 70 | { 71 | throw new NotImplementedException(); 72 | } 73 | 74 | public void PullFromRemote(string remoteUrl, string directory, ScmCredentials credentials) 75 | { 76 | throw new NotImplementedException(); 77 | } 78 | 79 | public bool RepositoryContainsCommit(string directory, string commitid) 80 | { 81 | throw new NotImplementedException(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ApiCallerTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters; 3 | using ScmBackup.Tests.Hosters; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Xunit; 8 | 9 | namespace ScmBackup.Tests 10 | { 11 | public class ApiCallerTests 12 | { 13 | private ConfigSource source1; 14 | private ConfigSource source2; 15 | private FakeContext context; 16 | 17 | private List list1; 18 | private List list2; 19 | private FakeHosterApiCaller hac; 20 | 21 | public ApiCallerTests() 22 | { 23 | var reader = new FakeConfigReader(); 24 | reader.SetDefaultFakeConfig(); 25 | source1 = reader.FakeConfig.Sources.First(); 26 | 27 | source2 = new ConfigSource(); 28 | source2.Title = "title2"; 29 | source2.Hoster = "fake"; 30 | reader.FakeConfig.Sources.Add(source2); 31 | 32 | this.context = new FakeContext(); 33 | this.context.Config = reader.ReadConfig(); 34 | 35 | list1 = new List(); 36 | list1.Add(new HosterRepository("foo1", "foo1", "http://foo1", ScmType.Git)); 37 | 38 | list2 = new List(); 39 | list2.Add(new HosterRepository("foo2", "foo2", "http://foo2", ScmType.Git)); 40 | 41 | hac = new FakeHosterApiCaller(); 42 | hac.Lists.Add(source1, list1); 43 | hac.Lists.Add(source2, list2); 44 | } 45 | 46 | [Fact] 47 | public void ExecutesHosterApiCallerForEachConfigSource() 48 | { 49 | var sut = new ApiCaller(hac, context); 50 | var result = sut.CallApis(); 51 | 52 | Assert.Equal(2, hac.PassedConfigSources.Count); 53 | Assert.Equal(2, result.Dic.Count()); 54 | 55 | var resultList1 = result.GetReposForSource(source1); 56 | Assert.Equal(list1, resultList1); 57 | 58 | var resultList2 = result.GetReposForSource(source2); 59 | Assert.Equal(list2, resultList2); 60 | } 61 | 62 | [Fact] 63 | public void ThrowsWhenNoHosterApiCallerIsPassed() 64 | { 65 | Assert.ThrowsAny(() => new ApiCaller(null, context)); 66 | } 67 | 68 | [Fact] 69 | public void ThrowsWhenNoContextIsPassed() 70 | { 71 | Assert.ThrowsAny(() => new ApiCaller(hac, null)); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ScmBackup/ScmBackup.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | ScmBackup 6 | Exe 7 | ScmBackup 8 | false 9 | false 10 | false 11 | false 12 | false 13 | false 14 | false 15 | false 16 | false 17 | Linux 18 | ..\.. 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | True 44 | True 45 | Resource.resx 46 | 47 | 48 | 49 | 50 | 51 | ResXFileCodeGenerator 52 | Resource.Designer.cs 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | 21 | jobs: 22 | analyze: 23 | name: Analyze 24 | runs-on: ubuntu-latest 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: [ 'csharp' ] 30 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 31 | # Learn more: 32 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 33 | 34 | steps: 35 | - name: Checkout repository 36 | uses: actions/checkout@v4 37 | 38 | # Initializes the CodeQL tools for scanning. 39 | - name: Initialize CodeQL 40 | uses: github/codeql-action/init@v3 41 | with: 42 | languages: ${{ matrix.language }} 43 | # If you wish to specify custom queries, you can do so here or in a config file. 44 | # By default, queries listed here will override any specified in a config file. 45 | # Prefix the list here with "+" to use these queries and those in the config file. 46 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 47 | 48 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 49 | # If this step fails, then you should remove it and run the build manually (see below) 50 | - name: Autobuild 51 | uses: github/codeql-action/autobuild@v3 52 | 53 | # ℹ️ Command-line programs to run using the OS shell. 54 | # 📚 https://git.io/JvXDl 55 | 56 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 57 | # and modify them (or add more) to build your code if your project 58 | # uses a compiled language 59 | 60 | #- run: | 61 | # make bootstrap 62 | # make release 63 | 64 | - name: Perform CodeQL Analysis 65 | uses: github/codeql-action/analyze@v3 66 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Hosters/BitbucketApiTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters.Bitbucket; 3 | using ScmBackup.Http; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Integration.Hosters 8 | { 9 | public class BitbucketApiTests : IHosterApiTests 10 | { 11 | // user, repo etc. 12 | internal override string HosterUser { get { return "scm-backup-testuser"; } } 13 | internal override string HosterOrganization { get { return "scm-backup-testteam"; } } 14 | internal override string HosterRepo { get { return "scm-backup-test-git"; } } 15 | internal override string HosterCommit { get { return "389dae62982075f97efb660824c31f712872a9cd"; } } 16 | internal override string HosterWikiCommit { get { return "8c621fd488ee5fa1ed19ca78113ccc92d55820bd"; } } 17 | internal override string HosterPaginationUser { get { return "evzijst"; } } 18 | internal override string HosterPrivateRepo { get { return TestHelper.EnvVar(this.EnvVarPrefix, "RepoPrivateGit"); } } 19 | 20 | internal override string EnvVarPrefix 21 | { 22 | get { return "Tests_Bitbucket"; } 23 | } 24 | 25 | internal override string ConfigHoster 26 | { 27 | get { return "bitbucket"; } 28 | } 29 | 30 | internal override int Pagination_MinNumberOfRepos 31 | { 32 | get { return 11; } // https://developer.atlassian.com/bitbucket/api/2/reference/meta/pagination 33 | } 34 | 35 | internal override bool SkipUnauthenticatedTests 36 | { 37 | get { return false; } 38 | } 39 | 40 | public BitbucketApiTests() 41 | { 42 | this.sut = new BitbucketApi(new HttpRequest()); 43 | } 44 | 45 | [Fact] 46 | public void GetRepositoryList_DoesntReturnMercurialRepos() 47 | { 48 | // #60: 2 months after Bitbucket's HG deprecation, their API still returns HG repos but cloning/pulling them fails -> ignore them 49 | var source = new ConfigSource(); 50 | source.Hoster = this.ConfigHoster; 51 | source.Type = "user"; 52 | source.Name = this.HosterUser; 53 | source.AuthName = TestHelper.EnvVar(this.EnvVarPrefix, "Name"); 54 | source.Password = TestHelper.EnvVar(this.EnvVarPrefix, "PW"); 55 | 56 | var repoList = sut.GetRepositoryList(source); 57 | 58 | var hgrepos = repoList.Where(x => x.Scm == ScmType.Mercurial); 59 | Assert.Empty(hgrepos); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ErrorHandlingScmBackupTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | 5 | namespace ScmBackup.Tests 6 | { 7 | public class ErrorHandlingScmBackupTests 8 | { 9 | static (FakeLogger FakeLogger, ErrorHandlingScmBackup ErrorHandlingScmBackup) BuildFakeScmBackup() 10 | { 11 | var ex = new Exception("!!!"); 12 | var subBackup = new FakeScmBackup(); 13 | subBackup.ToThrow = ex; 14 | 15 | var conf = new FakeConfigReader(); 16 | conf.SetDefaultFakeConfig(); 17 | 18 | var context = new FakeContext(); 19 | 20 | var logger = new FakeLogger(); 21 | 22 | var backup = new ErrorHandlingScmBackup(subBackup, logger, context); 23 | return (logger, backup); 24 | } 25 | 26 | [Fact] 27 | public void LogsWhenExceptionIsThrown() 28 | { 29 | var (logger, backup) = BuildFakeScmBackup(); 30 | 31 | backup.Run(); 32 | 33 | Assert.True(logger.LoggedSomething); 34 | Assert.Equal(ErrorLevel.Error, logger.LastErrorLevel); 35 | // we can't check whether the last exception is the exception from above, 36 | // because there are more logging outputs after the exception. 37 | } 38 | 39 | [Fact] 40 | public void ReturnsFalseWhenExceptionIsThrown() 41 | { 42 | var (_, backup) = BuildFakeScmBackup(); 43 | 44 | var result = backup.Run(); 45 | 46 | Assert.False(result); 47 | } 48 | 49 | [Fact] 50 | public void RunsExecuteOnExit_OnRegularExit() 51 | { 52 | var subBackup = new FakeScmBackup(); 53 | var context = new FakeContext(); 54 | var logger = new FakeLogger(); 55 | 56 | var sut = new ErrorHandlingScmBackup(subBackup, logger, context); 57 | sut.Run(); 58 | 59 | Assert.True(logger.ExecutedOnExit); 60 | Assert.True(logger.ExecuteOnExit_Successful); 61 | } 62 | 63 | [Fact] 64 | public void RunsExecuteOnExit_WhenExceptionIsThrown() 65 | { 66 | var subBackup = new FakeScmBackup(); 67 | subBackup.ToThrow = new Exception("!!"); 68 | 69 | var context = new FakeContext(); 70 | var logger = new FakeLogger(); 71 | 72 | var sut = new ErrorHandlingScmBackup(subBackup, logger, context); 73 | sut.Run(); 74 | 75 | Assert.True(logger.ExecutedOnExit); 76 | Assert.False(logger.ExecuteOnExit_Successful); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Scm/GitScmTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Scm; 2 | using ScmBackup.Tests.Hosters; 3 | using System; 4 | using System.IO; 5 | using Xunit; 6 | 7 | namespace ScmBackup.Tests.Integration.Scm 8 | { 9 | public class GitScmTests : IScmTests 10 | { 11 | public GitScmTests() 12 | { 13 | this.sut = new GitScm(new FileSystemHelper(), new FakeContext()); 14 | } 15 | 16 | internal override string PublicRepoUrl 17 | { 18 | get 19 | { 20 | string url = CloneUrlBuilder.GithubCloneUrl("scm-backup-testuser", "scm-backup"); 21 | return url; 22 | } 23 | } 24 | 25 | internal override string PrivateRepoUrl 26 | { 27 | get { return CloneUrlBuilder.BitbucketCloneUrl(TestHelper.EnvVar("Tests_Bitbucket_Name"), TestHelper.EnvVar("Tests_Bitbucket_RepoPrivateGit")); } 28 | } 29 | 30 | internal override ScmCredentials PrivateRepoCredentials 31 | { 32 | get { return new ScmCredentials(TestHelper.EnvVar("Tests_Bitbucket_Name"), TestHelper.EnvVar("Tests_Bitbucket_PW")); } 33 | } 34 | 35 | internal override string NonExistingRepoUrl 36 | { 37 | get { return CloneUrlBuilder.GithubCloneUrl("scm-backup-testuser", "repo-does-not-exist"); } 38 | } 39 | 40 | internal override string DotRepoUrl 41 | { 42 | get { return CloneUrlBuilder.GithubCloneUrl("scm-backup-testuser", "name-with-dot."); } 43 | } 44 | 45 | internal override string PublicRepoExistingCommitId 46 | { 47 | get 48 | { 49 | return "7be29139f4cdc4037647fc2f21d9d82c42a96e88"; 50 | } 51 | } 52 | 53 | internal override string PublicRepoNonExistingCommitId 54 | { 55 | get { return "00000"; } 56 | } 57 | 58 | [Fact] 59 | public void CreateRepository_WorksWithDotPath() 60 | { 61 | // Apparently `git init` fails when you pass a multi-level path that doesn't exist 62 | // yet, and where the name of one directory ends with "." 63 | // This happens when backing up a repo whose name ends with "." 64 | // The resulting path is "REPONAME.\repo" 65 | string maindir = DirectoryHelper.CreateTempDirectory(DirSuffix("create-git-dot")); 66 | var dir = Path.Combine(maindir, "dotdir.", "repo"); 67 | 68 | sut.CreateRepository(dir); 69 | 70 | Assert.True(sut.DirectoryIsRepository(dir)); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/Http/HttpRequestTests.cs: -------------------------------------------------------------------------------- 1 | using RichardSzalay.MockHttp; 2 | using ScmBackup.Http; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace ScmBackup.Tests.Http 10 | { 11 | public class HttpRequestTests 12 | { 13 | [Fact] 14 | public async Task RequestIsExecuted() 15 | { 16 | var mockHttp = new MockHttpMessageHandler(); 17 | mockHttp.Expect("http://foo.com/bar") 18 | .WithHeaders(new Dictionary 19 | { 20 | { "h1", "v1" }, 21 | { "h2", "v2" } 22 | }) 23 | .Respond(HttpStatusCode.OK, "application/json", "content"); 24 | 25 | var sut = new HttpRequest(); 26 | sut.HttpClient = new HttpClient(mockHttp); 27 | 28 | 29 | sut.SetBaseUrl("http://foo.com"); 30 | sut.AddHeader("h1", "v1"); 31 | sut.AddHeader("h2", "v2"); 32 | var result = await sut.Execute("bar"); 33 | 34 | 35 | Assert.NotNull(result); 36 | Assert.Equal(HttpStatusCode.OK, result.Status); 37 | Assert.True(result.IsSuccessStatusCode); 38 | Assert.Equal("content", result.Content); 39 | } 40 | 41 | [Fact] 42 | public async Task ReturnsContentOnError() 43 | { 44 | var mock = new MockHttpMessageHandler(); 45 | mock.Expect("http://foo.com/bar") 46 | .Respond(HttpStatusCode.NotFound, "application/json", "content"); 47 | 48 | var sut = new HttpRequest(); 49 | sut.HttpClient = new HttpClient(mock); 50 | 51 | sut.SetBaseUrl("http://foo.com"); 52 | var result = await sut.Execute("bar"); 53 | 54 | Assert.NotNull(result); 55 | Assert.Equal(HttpStatusCode.NotFound, result.Status); 56 | Assert.False(result.IsSuccessStatusCode); 57 | Assert.Equal("content", result.Content); 58 | } 59 | 60 | [Fact] 61 | public void AddBasicAuthHeader_AddsHeaderWithValues() 62 | { 63 | var sut = new HttpRequest(); 64 | sut.HttpClient = new HttpClient(); 65 | 66 | sut.AddBasicAuthHeader("user", "pass"); 67 | 68 | var authHeader = sut.HttpClient.DefaultRequestHeaders.Authorization; 69 | Assert.NotNull(authHeader); 70 | Assert.Equal("Basic", authHeader.Scheme); 71 | Assert.Equal("dXNlcjpwYXNz", authHeader.Parameter); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ScmBackup.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A94B6E7D-6E9E-4277-93B0-7D0686AA7CE3}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F5B5055F-0568-41E5-A10D-4611FD268D44}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScmBackup", "src\ScmBackup\ScmBackup.csproj", "{430B8951-BC74-40F9-8A7D-D3FE5CA361E2}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScmBackup.Tests", "src\ScmBackup.Tests\ScmBackup.Tests.csproj", "{52B54DBD-5A6F-4E5A-A0C7-B30BCF5A85F9}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScmBackup.Tests.Integration", "src\ScmBackup.Tests.Integration\ScmBackup.Tests.Integration.csproj", "{6E03432D-9856-4699-BC7A-BE60B615F720}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {430B8951-BC74-40F9-8A7D-D3FE5CA361E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {430B8951-BC74-40F9-8A7D-D3FE5CA361E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {430B8951-BC74-40F9-8A7D-D3FE5CA361E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {430B8951-BC74-40F9-8A7D-D3FE5CA361E2}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {52B54DBD-5A6F-4E5A-A0C7-B30BCF5A85F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {52B54DBD-5A6F-4E5A-A0C7-B30BCF5A85F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {52B54DBD-5A6F-4E5A-A0C7-B30BCF5A85F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {52B54DBD-5A6F-4E5A-A0C7-B30BCF5A85F9}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {6E03432D-9856-4699-BC7A-BE60B615F720}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {6E03432D-9856-4699-BC7A-BE60B615F720}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {6E03432D-9856-4699-BC7A-BE60B615F720}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {6E03432D-9856-4699-BC7A-BE60B615F720}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(NestedProjects) = preSolution 39 | {430B8951-BC74-40F9-8A7D-D3FE5CA361E2} = {A94B6E7D-6E9E-4277-93B0-7D0686AA7CE3} 40 | {52B54DBD-5A6F-4E5A-A0C7-B30BCF5A85F9} = {A94B6E7D-6E9E-4277-93B0-7D0686AA7CE3} 41 | {6E03432D-9856-4699-BC7A-BE60B615F720} = {A94B6E7D-6E9E-4277-93B0-7D0686AA7CE3} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Hosters/GitlabBackupTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters.Gitlab; 3 | using ScmBackup.Http; 4 | using ScmBackup.Scm; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using Xunit; 9 | 10 | namespace ScmBackup.Tests.Integration.Hosters 11 | { 12 | public class GitlabBackupTests : IBackupTests 13 | { 14 | private string prefix = "Tests_Gitlab"; 15 | 16 | internal override string PublicUserName { get { return "scm-backup-testuser"; } } 17 | internal override string PublicRepoName { get { return "scm-backup-test"; } } 18 | internal override string PrivateUserName { get { return TestHelper.EnvVar(prefix, "Name"); } } 19 | internal override string PrivateRepoName { get { return TestHelper.EnvVar(prefix, "RepoPrivate"); } } 20 | 21 | protected override void Setup(bool usePrivateRepo) 22 | { 23 | // re-use test repo for Api tests 24 | var s = new ConfigSource(); 25 | s.Hoster = "gitlab"; 26 | s.Type = "user"; 27 | s.Name = this.GetUserName(usePrivateRepo); 28 | s.AuthName = TestHelper.EnvVar(prefix, "Name"); 29 | s.Password= TestHelper.EnvVar(prefix, "PW"); 30 | this.source = s; 31 | 32 | var config = new Config(); 33 | config.Sources.Add(s); 34 | 35 | var context = new FakeContext(); 36 | context.Config = config; 37 | 38 | var factory = new FakeScmFactory(); 39 | factory.Register(ScmType.Git, new GitScm(new FileSystemHelper(), context)); 40 | 41 | var api = new GitlabApi(new HttpRequest(), factory); 42 | var repoList = api.GetRepositoryList(this.source); 43 | this.repo = repoList.Find(r=>r.ShortName == this.GetRepoName(usePrivateRepo)); 44 | 45 | this.scm = new GitScm(new FileSystemHelper(), context); 46 | Assert.True(this.scm.IsOnThisComputer()); 47 | 48 | var scmfactory = new FakeScmFactory(); 49 | scmfactory.Register(ScmType.Git, this.scm); 50 | this.sut = new GitlabBackup(scmfactory); 51 | } 52 | 53 | protected override void AssertRepo(string dir) 54 | { 55 | this.DefaultRepoAssert(dir, "d7c9ad8185b7707dbcc907e41154e3e5e5b2a540"); 56 | } 57 | 58 | protected override void AssertPrivateRepo(string dir) 59 | { 60 | this.DefaultRepoAssert(dir); 61 | } 62 | 63 | protected override void AssertWiki(string dir) 64 | { 65 | this.DefaultRepoAssert(dir, "5893873f9da26fc59bbeaafde5fad5800907e56f"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ScmBackup/Configuration/ConfigSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ScmBackup.Configuration 5 | { 6 | /// 7 | /// Configuration data to get the repositories of user X from hoster Y 8 | /// (subclass for Config) 9 | /// 10 | internal class ConfigSource 11 | { 12 | /// 13 | /// title of this config source (must be unique) 14 | /// 15 | public string Title { get; set; } 16 | 17 | /// 18 | /// name of the hoster 19 | /// 20 | public string Hoster { get; set; } 21 | 22 | /// 23 | /// user type (e.g. user/team) 24 | /// 25 | public string Type { get; set; } 26 | 27 | /// 28 | /// user name 29 | /// 30 | public string Name { get; set; } 31 | 32 | /// 33 | /// user name for api authentication, used only for Bitbucket 34 | /// See issue 84 35 | /// 36 | public string ApiAuthName { get; set; } 37 | 38 | /// 39 | /// user name for authentication 40 | /// (can be a different than the user whose repositories are backed up) 41 | /// 42 | public string AuthName { get; set; } 43 | 44 | /// 45 | /// list of repository names which should be ignored 46 | /// 47 | public List IgnoreRepos { get; set; } 48 | 49 | 50 | /// 51 | /// list of repository names which should be included in the backup, all others will be ignored! 52 | /// 53 | public List IncludeRepos { get; set; } 54 | 55 | /// 56 | /// password for authentication 57 | /// 58 | public string Password { get; set; } 59 | 60 | public bool IsAuthenticated 61 | { 62 | get 63 | { 64 | return !String.IsNullOrWhiteSpace(this.AuthName) && !String.IsNullOrWhiteSpace(this.Password); 65 | } 66 | } 67 | 68 | public override bool Equals(object obj) 69 | { 70 | if (obj == null) 71 | { 72 | return false; 73 | } 74 | 75 | var source = obj as ConfigSource; 76 | 77 | if (source == null) 78 | { 79 | return false; 80 | } 81 | 82 | return (source.Title == this.Title); 83 | } 84 | 85 | public override int GetHashCode() 86 | { 87 | return this.Title.GetHashCode(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests/ValidationResultTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace ScmBackup.Tests 6 | { 7 | public class ValidationResultTests 8 | { 9 | private ValidationResult sut; 10 | 11 | public ValidationResultTests() 12 | { 13 | sut = new ValidationResult(); 14 | } 15 | 16 | [Fact] 17 | public void AddMessageAddsSingleMessage() 18 | { 19 | sut.AddMessage(ErrorLevel.Info, "i"); 20 | 21 | Assert.Single(sut.Messages); 22 | } 23 | 24 | [Fact] 25 | public void AddMessageAddsConfigSourceTitle() 26 | { 27 | var source = new ConfigSource(); 28 | source.Title = "foo"; 29 | 30 | sut = new ValidationResult(source); 31 | sut.AddMessage(ErrorLevel.Info, "message"); 32 | 33 | var createdText = sut.Messages.First().Message; 34 | Assert.StartsWith("foo", createdText); 35 | Assert.EndsWith("message", createdText); 36 | } 37 | 38 | [Fact] 39 | public void IsValidIsTrueWithNoMessages() 40 | { 41 | Assert.False(sut.Messages.Any()); 42 | Assert.True(sut.IsValid); 43 | } 44 | 45 | [Fact] 46 | public void IsValidIsTrueWithInfoAndWarnMessage() 47 | { 48 | sut.AddMessage(ErrorLevel.Info, "i"); 49 | sut.AddMessage(ErrorLevel.Warn, "w"); 50 | 51 | Assert.Equal(2, sut.Messages.Count); 52 | Assert.True(sut.IsValid); 53 | } 54 | 55 | [Fact] 56 | public void IsValidIsFalseWithErrorMessage() 57 | { 58 | sut.AddMessage(ErrorLevel.Error, "e"); 59 | 60 | Assert.Single(sut.Messages); 61 | Assert.False(sut.IsValid); 62 | } 63 | 64 | [Fact] 65 | public void IsValidIsFalseWithAllMessages() 66 | { 67 | sut.AddMessage(ErrorLevel.Info, "i"); 68 | sut.AddMessage(ErrorLevel.Warn, "w"); 69 | sut.AddMessage(ErrorLevel.Error, "e"); 70 | 71 | Assert.Equal(3, sut.Messages.Count); 72 | Assert.False(sut.IsValid); 73 | } 74 | 75 | [Fact] 76 | public void AddMessageWithoutTypeSetsUndefined() 77 | { 78 | sut.AddMessage(ErrorLevel.Info, "foo"); 79 | 80 | Assert.Equal(ValidationMessageType.Undefined, sut.Messages.First().Type); 81 | } 82 | 83 | [Fact] 84 | public void AddMessageWithTypeSetsType() 85 | { 86 | sut.AddMessage(ErrorLevel.Info, "foo", ValidationMessageType.NameEmpty); 87 | 88 | Assert.Equal(ValidationMessageType.NameEmpty, sut.Messages.First().Type); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/ScmBackup/FileSystemHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace ScmBackup 7 | { 8 | /// 9 | /// helper class for file system operations 10 | /// 11 | public class FileSystemHelper : IFileSystemHelper 12 | { 13 | /// 14 | /// Checks whether the given directory is empty 15 | /// 16 | public bool DirectoryIsEmpty(string path) 17 | { 18 | if (Directory.GetFiles(path).Any() || Directory.GetDirectories(path).Any()) 19 | { 20 | return false; 21 | } 22 | 23 | return true; 24 | } 25 | 26 | /// 27 | /// wrapper for Directory.CreateDirectory 28 | /// 29 | public void CreateDirectory(string path) 30 | { 31 | Directory.CreateDirectory(path); 32 | } 33 | 34 | /// 35 | /// Creates a subdirectory inside the given directory and returns the path 36 | /// 37 | public string CreateSubDirectory(string mainDir, string subDir) 38 | { 39 | if (!Directory.Exists(mainDir)) 40 | { 41 | throw new DirectoryNotFoundException(string.Format(Resource.DirectoryDoesntExist, mainDir)); 42 | } 43 | 44 | string newDir = Path.Combine(mainDir, subDir); 45 | Directory.CreateDirectory(newDir); 46 | return newDir; 47 | } 48 | 49 | /// 50 | /// wrapper for Path.Combine 51 | /// 52 | public string PathCombine(string path1, string path2) 53 | { 54 | return Path.Combine(path1, path2); 55 | } 56 | 57 | /// 58 | /// Returns a list of all subdirectory names 59 | /// 60 | public IEnumerable GetSubDirectoryNames(string path) 61 | { 62 | var info = new DirectoryInfo(path); 63 | return info.GetDirectories().Select(x => x.Name); 64 | } 65 | 66 | /// 67 | /// Deletes a directory 68 | /// 69 | public void DeleteDirectory(string path) 70 | { 71 | // if the directory is a Git repo which was pulled into, Directory.Delete isn't able to delete it: https://stackoverflow.com/q/63449326/6884 72 | var directory = new DirectoryInfo(path) { Attributes = FileAttributes.Normal }; 73 | 74 | foreach (var info in directory.GetFileSystemInfos("*", SearchOption.AllDirectories)) 75 | { 76 | info.Attributes = FileAttributes.Normal; 77 | } 78 | 79 | directory.Delete(true); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/Hosters/BitbucketBackupGitTests.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | using ScmBackup.Hosters.Bitbucket; 3 | using ScmBackup.Http; 4 | using ScmBackup.Scm; 5 | using System.IO; 6 | using Xunit; 7 | 8 | namespace ScmBackup.Tests.Integration.Hosters 9 | { 10 | public class BitbucketBackupGitTests : IBackupTests 11 | { 12 | private string prefix = "Tests_Bitbucket"; 13 | 14 | internal override string PublicUserName { get { return "scm-backup-testuser"; } } 15 | internal override string PublicRepoName { get { return "scm-backup-test-git"; } } 16 | 17 | internal override string PrivateUserName { get { return TestHelper.EnvVar(prefix, "Name"); } } 18 | internal override string PrivateRepoName { get { return TestHelper.EnvVar(prefix, "RepoPrivateGit"); } } 19 | 20 | protected override void Setup(bool usePrivateRepo) 21 | { 22 | // re-use test repo for Api tests 23 | this.source = new ConfigSource(); 24 | this.source.Hoster = "bitbucket"; 25 | this.source.Type = "user"; 26 | this.source.Name = this.GetUserName(usePrivateRepo); 27 | this.source.AuthName = TestHelper.EnvVar(prefix, "Name"); 28 | this.source.Password = TestHelper.EnvVar(prefix, "PW"); 29 | 30 | var config = new Config(); 31 | config.Sources.Add(this.source); 32 | 33 | var context = new FakeContext(); 34 | context.Config = config; 35 | 36 | var api = new BitbucketApi(new HttpRequest()); 37 | var repoList = api.GetRepositoryList(this.source); 38 | this.repo = repoList.Find(r => r.ShortName == this.GetRepoName(usePrivateRepo)); 39 | 40 | this.scm = new GitScm(new FileSystemHelper(), context); 41 | Assert.True(this.scm.IsOnThisComputer()); 42 | 43 | var scmFactory = new FakeScmFactory(); 44 | scmFactory.Register(ScmType.Git, this.scm); 45 | this.sut = new BitbucketBackup(scmFactory); 46 | } 47 | 48 | protected override void AssertRepo(string dir) 49 | { 50 | Assert.True(Directory.Exists(dir)); 51 | Assert.True(this.scm.DirectoryIsRepository(dir)); 52 | Assert.True(scm.RepositoryContainsCommit(dir, "389dae62982075f97efb660824c31f712872a9cd")); 53 | } 54 | 55 | protected override void AssertWiki(string dir) 56 | { 57 | Assert.True(Directory.Exists(dir)); 58 | Assert.True(this.scm.DirectoryIsRepository(dir)); 59 | Assert.True(scm.RepositoryContainsCommit(dir, "8c621fd488ee5fa1ed19ca78113ccc92d55820bd")); 60 | } 61 | 62 | protected override void AssertPrivateRepo(string dir) 63 | { 64 | Assert.True(Directory.Exists(dir)); 65 | Assert.True(this.scm.DirectoryIsRepository(dir)); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ScmBackup.Tests.Integration/ScmBackup.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | ScmBackup.Tests.Integration 6 | ScmBackup.Tests.Integration 7 | true 8 | false 9 | false 10 | false 11 | false 12 | false 13 | false 14 | false 15 | false 16 | false 17 | 18 | 19 | 20 | 21 | PreserveNewest 22 | 23 | 24 | Always 25 | 26 | 27 | Always 28 | 29 | 30 | Always 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | Always 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | all 52 | runtime; build; native; contentfiles; analyzers; buildtransitive 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/HosterRepository.cs: -------------------------------------------------------------------------------- 1 | namespace ScmBackup.Hosters 2 | { 3 | /// 4 | /// Data to access one single repository 5 | /// 6 | internal class HosterRepository 7 | { 8 | public HosterRepository(string fullName, string shortName, string cloneUrl, ScmType scm) 9 | { 10 | SetFullName(fullName); 11 | this.ShortName = shortName; 12 | this.CloneUrl = cloneUrl; 13 | this.Scm = scm; 14 | } 15 | 16 | public HosterRepository(string fullName, string shortName, string cloneUrl, ScmType scm, bool haswiki, string wikiurl, bool hasissues, string issueurl) 17 | { 18 | SetFullName(fullName); 19 | this.ShortName = shortName; 20 | this.CloneUrl = cloneUrl; 21 | this.Scm = scm; 22 | SetWiki(haswiki, wikiurl); 23 | SetIssues(hasissues, issueurl); 24 | } 25 | 26 | /// 27 | /// Full name of the repository (e.g. "username/reponame") 28 | /// 29 | public string FullName { get; private set; } 30 | 31 | /// 32 | /// "short name" of the repository (e.g. "reponame") 33 | /// 34 | public string ShortName { get; private set; } 35 | 36 | /// 37 | /// URL to clone the repository 38 | /// 39 | public string CloneUrl { get; private set; } 40 | 41 | /// 42 | /// The SCM of the repository 43 | /// 44 | public ScmType Scm { get; private set; } 45 | 46 | /// 47 | /// Does the repo have a wiki? 48 | /// 49 | public bool HasWiki { get; private set; } 50 | 51 | /// 52 | /// URL to backup the wiki, if one exists) 53 | /// 54 | public string WikiUrl { get; private set; } 55 | 56 | /// 57 | /// Does the repo have issues? 58 | /// 59 | public bool HasIssues { get; private set; } 60 | 61 | /// 62 | /// URL to backup the issues 63 | /// 64 | public string IssueUrl { get; private set; } 65 | 66 | /// 67 | /// the repo is private 68 | /// 69 | public bool IsPrivate { get; private set; } 70 | 71 | public void SetFullName(string name) 72 | { 73 | this.FullName = name.Replace('/', '#'); 74 | } 75 | 76 | public void SetWiki(bool haswiki, string wikiurl) 77 | { 78 | this.HasWiki = haswiki; 79 | this.WikiUrl = wikiurl; 80 | } 81 | 82 | public void SetIssues(bool hasissues, string issueurl) 83 | { 84 | this.HasIssues = hasissues; 85 | this.IssueUrl = issueurl; 86 | } 87 | 88 | public void SetPrivate(bool isPrivate) 89 | { 90 | this.IsPrivate = isPrivate; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ScmBackup/Hosters/ConfigSourceValidatorBase.cs: -------------------------------------------------------------------------------- 1 | using ScmBackup.Configuration; 2 | 3 | namespace ScmBackup.Hosters 4 | { 5 | /// 6 | /// base class for all config source validators 7 | /// 8 | internal abstract class ConfigSourceValidatorBase : IConfigSourceValidator 9 | { 10 | /// 11 | /// name of the hoster (the "hoster" value from the config source) 12 | /// 13 | public abstract string HosterName { get; } 14 | 15 | /// 16 | /// Some APIs require authentication with the same user whose repositories are requested. In this case, set this to True 17 | /// 18 | public abstract bool AuthNameAndNameMustBeEqual { get; } 19 | 20 | /// 21 | /// basic validation rules which are always the same 22 | /// 23 | public ValidationResult Validate(ConfigSource source) 24 | { 25 | var result = new ValidationResult(source); 26 | 27 | if (source.Hoster != this.HosterName) 28 | { 29 | result.AddMessage(ErrorLevel.Error, string.Format(Resource.WrongHoster, source.Hoster), ValidationMessageType.WrongHoster); 30 | } 31 | 32 | if (source.Type != "user" && source.Type != "org") 33 | { 34 | result.AddMessage(ErrorLevel.Error, string.Format(Resource.WrongType, source.Type), ValidationMessageType.WrongType); 35 | } 36 | 37 | if (string.IsNullOrWhiteSpace(source.Name)) 38 | { 39 | result.AddMessage(ErrorLevel.Error, Resource.NameEmpty, ValidationMessageType.NameEmpty); 40 | } 41 | 42 | bool authNameEmpty = string.IsNullOrWhiteSpace(source.AuthName); 43 | bool passwordEmpty = string.IsNullOrWhiteSpace(source.Password); 44 | 45 | if (authNameEmpty != passwordEmpty) 46 | { 47 | result.AddMessage(ErrorLevel.Error, Resource.AuthNameOrPasswortEmpty, ValidationMessageType.AuthNameOrPasswortEmpty); 48 | } 49 | else if (authNameEmpty && passwordEmpty) 50 | { 51 | result.AddMessage(ErrorLevel.Warn, Resource.AuthNameAndPasswortEmpty, ValidationMessageType.AuthNameAndPasswortEmpty); 52 | } 53 | 54 | if (this.AuthNameAndNameMustBeEqual) 55 | { 56 | if (source.Type != "org" && source.Name != source.AuthName) 57 | { 58 | result.AddMessage(ErrorLevel.Warn, string.Format(Resource.AuthNameAndNameNotEqual, source.Hoster), ValidationMessageType.AuthNameAndNameNotEqual); 59 | } 60 | } 61 | 62 | this.ValidateSpecific(result, source); 63 | 64 | return result; 65 | } 66 | 67 | /// 68 | /// hoster-specific validation rules - this CAN be implemented in the child classes IF the given hoster has special rules 69 | /// 70 | public virtual void ValidateSpecific(ValidationResult result, ConfigSource source) { } 71 | } 72 | } 73 | --------------------------------------------------------------------------------