├── 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 | 
4 |
5 | [](https://ci.appveyor.com/project/ChristianSpecht/scm-backup)
6 | [](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 |
--------------------------------------------------------------------------------