Przeglądaj źródła

Build projects API

Robert Marshall 4 lat temu
commit
8c3760500f
49 zmienionych plików z 1416 dodań i 0 usunięć
  1. 51 0
      .drone.yml
  2. 198 0
      .editorconfig
  3. 1 0
      .gitattributes
  4. 2 0
      .gitconfig
  5. 56 0
      .gitignore
  6. 1 0
      .nuke
  7. 9 0
      Readme.md
  8. 16 0
      api.projects.service
  9. 68 0
      build.ps1
  10. 62 0
      build.sh
  11. 10 0
      build/.editorconfig
  12. 62 0
      build/Build.cs
  13. 34 0
      build/_build.csproj
  14. 6 0
      clean-config.sh
  15. 109 0
      src/Robware.Api.Projects.Tests/CodeControllerTests.cs
  16. 29 0
      src/Robware.Api.Projects.Tests/Robware.Api.Projects.Tests.csproj
  17. 53 0
      src/Robware.Api.Projects.sln
  18. 40 0
      src/Robware.Api.Projects/Controllers/CodeController.cs
  19. 14 0
      src/Robware.Api.Projects/Program.cs
  20. 28 0
      src/Robware.Api.Projects/Properties/launchSettings.json
  21. 13 0
      src/Robware.Api.Projects/Robware.Api.Projects.csproj
  22. 46 0
      src/Robware.Api.Projects/Startup.cs
  23. 11 0
      src/Robware.Api.Projects/appsettings.Development.json
  24. 12 0
      src/Robware.Api.Projects/appsettings.json
  25. 114 0
      src/Robware.Projects.Gogs.Tests/GogsApiTests.cs
  26. 64 0
      src/Robware.Projects.Gogs.Tests/HttpClientBuilder.cs
  27. 22 0
      src/Robware.Projects.Gogs.Tests/Robware.Projects.Gogs.Tests.csproj
  28. 9 0
      src/Robware.Projects.Gogs/CommitNotFoundException.cs
  29. 50 0
      src/Robware.Projects.Gogs/GogsApi.cs
  30. 12 0
      src/Robware.Projects.Gogs/Models/GogsBranch.cs
  31. 19 0
      src/Robware.Projects.Gogs/Models/GogsCommit.cs
  32. 11 0
      src/Robware.Projects.Gogs/Models/GogsRepository.cs
  33. 9 0
      src/Robware.Projects.Gogs/RepositoryNotFoundException.cs
  34. 19 0
      src/Robware.Projects.Gogs/Robware.Projects.Gogs.csproj
  35. 10 0
      src/Robware.Projects.Gogs/States/AuthorState.cs
  36. 7 0
      src/Robware.Projects.Gogs/States/BranchState.cs
  37. 11 0
      src/Robware.Projects.Gogs/States/CommitDetailsState.cs
  38. 6 0
      src/Robware.Projects.Gogs/States/CommitPointerState.cs
  39. 16 0
      src/Robware.Projects.Gogs/States/CommitState.cs
  40. 10 0
      src/Robware.Projects.Gogs/States/RepositoryOwnerState.cs
  41. 7 0
      src/Robware.Projects.Gogs/States/RepositoryPermissionsState.cs
  42. 31 0
      src/Robware.Projects.Gogs/States/RepositoryState.cs
  43. 10 0
      src/Robware.Projects.Gogs/States/UserState.cs
  44. 9 0
      src/Robware.Projects.Gogs/UserNotFoundException.cs
  45. 6 0
      src/Robware.Projects/Code/Branch.cs
  46. 10 0
      src/Robware.Projects/Code/Commit.cs
  47. 10 0
      src/Robware.Projects/Code/IGitApi.cs
  48. 6 0
      src/Robware.Projects/Code/Repository.cs
  49. 7 0
      src/Robware.Projects/Robware.Projects.csproj

+ 51 - 0
.drone.yml

@@ -0,0 +1,51 @@
+kind: pipeline
+name: default
+
+clone:
+  skip_verify: true
+
+volumes:
+- name: output
+  host:
+    path: /var/www/Api.Projects
+
+steps:
+- name: build and publish
+  image: mcr.microsoft.com/dotnet/core/sdk:3.1
+  volumes:
+  - name: output
+    path: /output
+  environment:
+    GitApiEndpoint:
+      from_secret: GitApiEndpoint
+    GitApiToken:
+      from_secret: GitApiToken
+  commands:
+  - chmod +x ./build.sh
+  - ./build.sh
+  - sed -i "s/<GitApiEndpoint>/$GitApiEndpoint/g" output/appsettings.json
+  - sed -i "s/<GitApiToken>/$GitApiToken/g" output/appsettings.json
+  - cp api.projects.service output/
+  - cp -r ./output/* /output
+- name: restart service
+  privileged: true
+  image: appleboy/drone-ssh
+  settings:
+    host: 192.168.1.3
+    username:
+      from_secret: ssh_user
+    password:
+      from_secret: ssh_password
+    script:
+    - systemctl daemon-reload
+    - service api.projects restart
+- name: notify
+  image: drillster/drone-email
+  settings:
+    host: 192.168.1.3
+    skip_verify: true
+    from: build@robware.uk
+  when:
+    status:
+    - changed
+    - failure

+ 198 - 0
.editorconfig

@@ -0,0 +1,198 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+[*]
+indent_style = tab
+
+# C# files
+[*.cs]
+max_line_length = 180
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:suggestion
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:silent
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_local_function = true:suggestion
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
+
+# Code-block preferences
+csharp_prefer_braces = false:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = none
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+[*.{yaml,yml}]
+indent_style = space
+indent_size = 2

+ 1 - 0
.gitattributes

@@ -0,0 +1 @@
+src/Robware.Api.Projects/appsettings.Development.json	filter=clean-config

+ 2 - 0
.gitconfig

@@ -0,0 +1,2 @@
+[filter "clean-config"]
+	clean = ./clean-config.sh

+ 56 - 0
.gitignore

@@ -0,0 +1,56 @@
+*.swp
+*.*~
+project.lock.json
+.DS_Store
+*.pyc
+nupkg/
+
+# Visual Studio Code
+.vscode
+
+# Rider
+.idea
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+#build/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+msbuild.log
+msbuild.err
+msbuild.wrn
+
+# Visual Studio 2015
+.vs/
+
+
+#lcov files
+lcov.info
+
+#compiled and minified assets
+*.css
+*.min.css
+*.min.js
+
+#nodejs
+node_modules
+
+output
+.tmp
+*.ncrunchsolution
+/_NCrunch_Website
+*.DotSettings
+*.cache

+ 1 - 0
.nuke

@@ -0,0 +1 @@
+src/Robware.Api.Projects.sln

+ 9 - 0
Readme.md

@@ -0,0 +1,9 @@
+[![Build Status](https://build.robware.uk/api/badges/robware/Api.Projects/status.svg)](https://build.robware.uk/robware/Api.Projects)
+
+# Projects microservice
+
+Provides an API to retreive information from source control
+
+## Setup
+
+After clone, please run `git config --local include.path ../.gitconfig`. This will set up the filters required to ignore local dev config.

+ 16 - 0
api.projects.service

@@ -0,0 +1,16 @@
+[Unit]
+Description=Robware Projects API
+
+[Service]
+WorkingDirectory=/var/www/Api.Projects
+ExecStart=/usr/bin/dotnet /var/www/Api.Projects/Robware.Api.Projects.dll
+Restart=always
+# Restart service after 10 seconds if the dotnet service crashes:
+RestartSec=10
+KillSignal=SIGINT
+SyslogIdentifier=Api.Projects
+User=www-data
+Environment=ASPNETCORE_ENVIRONMENT=Production
+
+[Install]
+WantedBy=multi-user.target

+ 68 - 0
build.ps1

@@ -0,0 +1,68 @@
+[CmdletBinding()]
+Param(
+    [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+    [string[]]$BuildArguments
+)
+
+Write-Output "Windows PowerShell $($Host.Version)"
+
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 }
+$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
+$TempDirectory = "$PSScriptRoot\\.tmp"
+
+$DotNetGlobalFile = "$PSScriptRoot\\global.json"
+$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1"
+$DotNetChannel = "Current"
+
+$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
+$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function ExecSafe([scriptblock] $cmd) {
+    & $cmd
+    if ($LASTEXITCODE) { exit $LASTEXITCODE }
+}
+
+# If global.json exists, load expected version
+if (Test-Path $DotNetGlobalFile) {
+    $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+    if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+        $DotNetVersion = $DotNetGlobal.sdk.version
+    }
+}
+
+# If dotnet is installed locally, and expected version is not set or installation matches the expected version
+if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and `
+     (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) {
+    $env:DOTNET_EXE = (Get-Command "dotnet").Path
+}
+else {
+    $DotNetDirectory = "$TempDirectory\dotnet-win"
+    $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
+
+    # Download install script
+    $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
+    md -force $TempDirectory > $null
+    (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
+
+    # Install by channel or version
+    if (!(Test-Path variable:DotNetVersion)) {
+        ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+    } else {
+        ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
+    }
+}
+
+Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
+
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

+ 62 - 0
build.sh

@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+echo $(bash --version 2>&1 | head -n 1)
+
+set -eo pipefail
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.tmp"
+
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh"
+DOTNET_CHANNEL="Current"
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function FirstJsonValue {
+    perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2}
+}
+
+# If global.json exists, load expected version
+if [ -f "$DOTNET_GLOBAL_FILE" ]; then
+    DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE"))
+    if [ "$DOTNET_VERSION" == ""  ]; then
+        unset DOTNET_VERSION
+    fi
+fi
+
+# If dotnet is installed locally, and expected version is not set or installation matches the expected version
+if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then
+    export DOTNET_EXE="$(command -v dotnet)"
+else
+    DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+    export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+    
+    # Download install script
+    DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+    mkdir -p "$TEMP_DIRECTORY"
+    curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+    chmod +x "$DOTNET_INSTALL_FILE"
+    
+    # Install by channel or version
+    if [ -z ${DOTNET_VERSION+x} ]; then
+        "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+    else
+        "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
+    fi
+fi
+
+echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
+
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

+ 10 - 0
build/.editorconfig

@@ -0,0 +1,10 @@
+[*.cs]
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_require_accessibility_modifiers = never:warning
+
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning

+ 62 - 0
build/Build.cs

@@ -0,0 +1,62 @@
+using Nuke.Common;
+using Nuke.Common.Execution;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tools.DotNet;
+using Nuke.Common.Utilities.Collections;
+using static Nuke.Common.IO.FileSystemTasks;
+using static Nuke.Common.Tools.DotNet.DotNetTasks;
+
+[CheckBuildProjectConfigurations]
+[UnsetVisualStudioEnvironmentVariables]
+class Build : NukeBuild {
+	public static int Main() => Execute<Build>(x => x.Publish);
+
+	[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
+	readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
+
+	[Solution] readonly Solution Solution;
+
+	AbsolutePath SourceDirectory => RootDirectory / "src";
+	AbsolutePath OutputDirectory => RootDirectory / "output";
+
+	Target Clean => _ => _
+		.Before(Restore)
+		.Executes(() => {
+			SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
+			EnsureCleanDirectory(OutputDirectory);
+		});
+
+	Target Restore => _ => _
+		.Executes(() => {
+			DotNetRestore(s => s
+				.SetProjectFile(Solution));
+		});
+
+	Target Compile => _ => _
+		.DependsOn(Restore)
+		.Executes(() => {
+			DotNetBuild(s => s
+				.SetProjectFile(Solution)
+				.SetConfiguration(Configuration)
+				.EnableNoRestore());
+		});
+
+	Target Test => _ => _
+		.DependsOn(Compile)
+		.Executes(() => {
+			DotNetTest(s => s
+				.SetProjectFile(Solution)
+				.EnableNoRestore());
+		});
+
+	Target Publish => _ => _
+		.DependsOn(Test)
+		.Executes(() => {
+			DotNetPublish(s => s
+				.SetProject(SourceDirectory / "Robware.Api.Projects/Robware.Api.Projects.csproj")
+				.SetConfiguration(Configuration)
+				.SetOutput(OutputDirectory));
+		});
+
+}

+ 34 - 0
build/_build.csproj

@@ -0,0 +1,34 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <RootNamespace></RootNamespace>
+    <IsPackable>False</IsPackable>
+    <NoWarn>CS0649;CS0169</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Nuke.Common" Version="0.24.8" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <NukeSpecificationFiles Include="**\*.json" Exclude="bin\**;obj\**" />
+    <NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
+    <None Remove="*.csproj.DotSettings;*.ref.*.txt" />
+
+    <!-- Common build related files -->
+    <None Include="..\build.ps1" />
+    <None Include="..\build.sh" />
+    <None Include="..\.nuke" />
+    <None Include="..\global.json" Condition="Exists('..\global.json')" />
+    <None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
+    <None Include="..\azure-pipelines.yml" Condition="Exists('..\azure-pipelines.yml')" />
+    <None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />
+    <None Include="..\appveyor.yml" Condition="Exists('..\appveyor.yml')" />
+    <None Include="..\.travis.yml" Condition="Exists('..\.travis.yml')" />
+    <None Include="..\GitVersion.yml" Condition="Exists('..\GitVersion.yml')" />
+  </ItemGroup>
+
+</Project>

+ 6 - 0
clean-config.sh

@@ -0,0 +1,6 @@
+#!/bin/sh
+
+sed \
+-e 's/"gitApiEndpoint": ".*"/"gitApiEndpoint": ""/g' \
+-e 's/"gitApiToken": ".*"/"gitApiToken": ""/g' \
+$1

+ 109 - 0
src/Robware.Api.Projects.Tests/CodeControllerTests.cs

@@ -0,0 +1,109 @@
+using System;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using NSubstitute;
+using NSubstitute.ExceptionExtensions;
+using Robware.Api.Projects.Controllers;
+using Robware.Projects.Code;
+using Robware.Projects.Gogs;
+using Xunit;
+
+namespace Robware.Api.Projects.Tests {
+	public class CodeControllerTests {
+		private class TestRepository : Repository {
+			public TestRepository(string name, string url) {
+				Name = name;
+				Url = url;
+			}
+		}
+
+		private class TestBranch : Branch {
+			public TestBranch(string name, Commit commit) {
+				Name = name;
+				Commit = commit;
+			}
+		}
+
+		private class TestCommit : Commit {
+			public TestCommit(string id, string message, DateTimeOffset timestamp, string url = null) {
+				Id = id;
+				Message = message;
+				Timestamp = timestamp;
+				Url = url;
+			}
+		}
+
+		[Fact]
+		public async Task Repositories_WithUser_ReturnsRepositoryCollection() {
+			var logger = Substitute.For<ILogger<CodeController>>();
+			var api = Substitute.For<IGitApi>();
+			api.GetRepositories("test").Returns(new[] { new TestRepository("test", "url") });
+
+			var expectation = new[] { new TestRepository("test", "url") };
+
+			var controller = new CodeController(logger, api);
+			(await controller.Repositories("test")).Value.Should().BeEquivalentTo(expectation);
+		}
+
+		[Fact]
+		public async Task Repositories_WithUserThatDoesntExist_Returns404() {
+			var logger = Substitute.For<ILogger<CodeController>>();
+			var api = Substitute.For<IGitApi>();
+			api.GetRepositories("test").Throws(new UserNotFoundException("", null));
+
+			var controller = new CodeController(logger, api);
+			(await controller.Repositories("test")).Result.Should().BeOfType<NotFoundResult>();
+		}
+
+		[Fact]
+		public async Task Branches_WithUserAndRepository_ReturnsBranchCollection() {
+			var logger = Substitute.For<ILogger<CodeController>>();
+			var api = Substitute.For<IGitApi>();
+			api.GetBranches("user", "test").Returns(new[] {
+				new TestBranch("master", new TestCommit("0923b554309ef562fca978c7e981b3812bc4af40", "message", new DateTimeOffset(2020, 04, 11, 14, 09, 29, TimeSpan.FromHours(1))))
+			});
+
+			var expectation = new[] {
+				new TestBranch("master", new TestCommit("0923b554309ef562fca978c7e981b3812bc4af40", "message", new DateTimeOffset(2020, 04, 11, 14, 09, 29, TimeSpan.FromHours(1))))
+			};
+
+			var controller = new CodeController(logger, api);
+			(await controller.Branches("user", "test")).Value.Should().BeEquivalentTo(expectation);
+		}
+
+		[Fact]
+		public async Task Branches_WithUserOrRepositoryThatDoesntExist_Returns404() {
+			var logger = Substitute.For<ILogger<CodeController>>();
+			var api = Substitute.For<IGitApi>();
+			api.GetBranches("test", "404").Throws(new RepositoryNotFoundException("", "", null));
+
+			var controller = new CodeController(logger, api);
+			(await controller.Branches("test", "404")).Result.Should().BeOfType<NotFoundResult>();
+		}
+
+		[Fact]
+		public async Task Branches_WithUserAndRepositoryAndCommit_ReturnsBranchCollection() {
+			var logger = Substitute.For<ILogger<CodeController>>();
+			var api = Substitute.For<IGitApi>();
+			api.GetCommit("user", "test", "hash")
+			   .Returns(new TestCommit("0923b554309ef562fca978c7e981b3812bc4af40", "message", new DateTimeOffset(2020, 04, 11, 14, 09, 29, TimeSpan.FromHours(1))));
+
+			var expectation = new TestCommit("0923b554309ef562fca978c7e981b3812bc4af40", "message", new DateTimeOffset(2020, 04, 11, 14, 09, 29, TimeSpan.FromHours(1)));
+
+			var controller = new CodeController(logger, api);
+			(await controller.Commit("user", "test", "hash")).Value.Should().BeEquivalentTo(expectation);
+		}
+
+		[Fact]
+		public async Task Branches_WithUserOrRepositoryOrCommitThatDoesntExist_Returns404() {
+			var logger = Substitute.For<ILogger<CodeController>>();
+			var api = Substitute.For<IGitApi>();
+			api.GetCommit("test", "404", "hash").Throws(new CommitNotFoundException("", "", "", null));
+
+			var controller = new CodeController(logger, api);
+			(await controller.Commit("test", "404", "hash")).Result.Should().BeOfType<NotFoundResult>();
+		}
+	}
+}

+ 29 - 0
src/Robware.Api.Projects.Tests/Robware.Api.Projects.Tests.csproj

@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="FluentAssertions" Version="5.10.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+    <PackageReference Include="NSubstitute" Version="4.2.1" />
+    <PackageReference Include="xunit" Version="2.4.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
+    <PackageReference Include="coverlet.collector" Version="1.0.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Robware.Api.Projects\Robware.Api.Projects.csproj" />
+    <ProjectReference Include="..\Robware.Projects\Robware.Projects.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.Extensions.Logging.Abstractions">
+      <HintPath>C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.1.0\ref\netcoreapp3.1\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
+    </Reference>
+  </ItemGroup>
+
+</Project>

+ 53 - 0
src/Robware.Api.Projects.sln

@@ -0,0 +1,53 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29613.14
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robware.Api.Projects", "Robware.Api.Projects\Robware.Api.Projects.csproj", "{DD0C5A40-8E42-4046-A36E-D6A4F9291900}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Robware.Api.Projects.Tests", "Robware.Api.Projects.Tests\Robware.Api.Projects.Tests.csproj", "{6CEB61CD-19B5-4104-BA77-D24980E03400}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Projects", "Robware.Projects\Robware.Projects.csproj", "{A380C8FB-20EB-41D7-9310-9B33BDCB6A0F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Projects.Gogs", "Robware.Projects.Gogs\Robware.Projects.Gogs.csproj", "{FEF1E250-4F42-4335-9462-9606F4B358CB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Robware.Projects.Gogs.Tests", "Robware.Projects.Gogs.Tests\Robware.Projects.Gogs.Tests.csproj", "{5A6BB452-6F2A-44F7-BF5A-514C95608F2D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{06BF28D8-AAE9-4727-BF4F-198AFF52AEBF}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{06BF28D8-AAE9-4727-BF4F-198AFF52AEBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{06BF28D8-AAE9-4727-BF4F-198AFF52AEBF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DD0C5A40-8E42-4046-A36E-D6A4F9291900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{DD0C5A40-8E42-4046-A36E-D6A4F9291900}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{DD0C5A40-8E42-4046-A36E-D6A4F9291900}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{DD0C5A40-8E42-4046-A36E-D6A4F9291900}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6CEB61CD-19B5-4104-BA77-D24980E03400}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6CEB61CD-19B5-4104-BA77-D24980E03400}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6CEB61CD-19B5-4104-BA77-D24980E03400}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6CEB61CD-19B5-4104-BA77-D24980E03400}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A380C8FB-20EB-41D7-9310-9B33BDCB6A0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A380C8FB-20EB-41D7-9310-9B33BDCB6A0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A380C8FB-20EB-41D7-9310-9B33BDCB6A0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A380C8FB-20EB-41D7-9310-9B33BDCB6A0F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{FEF1E250-4F42-4335-9462-9606F4B358CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{FEF1E250-4F42-4335-9462-9606F4B358CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{FEF1E250-4F42-4335-9462-9606F4B358CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{FEF1E250-4F42-4335-9462-9606F4B358CB}.Release|Any CPU.Build.0 = Release|Any CPU
+		{5A6BB452-6F2A-44F7-BF5A-514C95608F2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{5A6BB452-6F2A-44F7-BF5A-514C95608F2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{5A6BB452-6F2A-44F7-BF5A-514C95608F2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{5A6BB452-6F2A-44F7-BF5A-514C95608F2D}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {F9052B29-4197-4FFC-A5D7-C4DFDF0DD5BD}
+	EndGlobalSection
+EndGlobal

+ 40 - 0
src/Robware.Api.Projects/Controllers/CodeController.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using Robware.Projects.Code;
+using Robware.Projects.Gogs;
+
+namespace Robware.Api.Projects.Controllers {
+	[ApiController]
+	[Route("[controller]")]
+	public class CodeController : ControllerBase {
+		private readonly ILogger<CodeController> _logger;
+		private readonly IGitApi _gitApi;
+
+		public CodeController(ILogger<CodeController> logger, IGitApi gitApi) {
+			_logger = logger;
+			_gitApi = gitApi;
+		}
+
+		private async Task<ActionResult<TResult>> Do<TResult, TException>(Func<Task<TResult>> method) where TException : Exception {
+			try {
+				return await method();
+			}
+			catch (TException e) {
+				_logger.Log(LogLevel.Error, e.Message);
+				return NotFound();
+			}
+		}
+
+		[HttpGet(nameof(Repositories))]
+		public async Task<ActionResult<Repository[]>> Repositories(string user) => await Do<Repository[], UserNotFoundException>(async ()=> (await _gitApi.GetRepositories(user)).ToArray());
+
+		[HttpGet(nameof(Branches))]
+		public async Task<ActionResult<Branch[]>> Branches(string user, string repository) => await Do<Branch[], RepositoryNotFoundException>(async() => (await _gitApi.GetBranches(user, repository)).ToArray());
+
+		[HttpGet(nameof(Commit))]
+		public async Task<ActionResult<Commit>> Commit(string user, string repository, string hash) => await Do<Commit, CommitNotFoundException>(async () => await _gitApi.GetCommit(user, repository, hash));
+	}
+}

+ 14 - 0
src/Robware.Api.Projects/Program.cs

@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace Robware.Api.Projects {
+	public class Program {
+		public static void Main(string[] args) =>
+			Host.CreateDefaultBuilder(args)
+				.ConfigureWebHostDefaults(webBuilder => {
+					webBuilder.UseStartup<Startup>();
+				})
+				.Build()
+				.Run();
+	}
+}

+ 28 - 0
src/Robware.Api.Projects/Properties/launchSettings.json

@@ -0,0 +1,28 @@
+{
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:63705",
+      "sslPort": 44380
+    }
+  },
+  "$schema": "http://json.schemastore.org/launchsettings.json",
+  "profiles": {
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "Robware.Api.Projects": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "launchUrl": "weatherforecast",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "https://localhost:5001;http://localhost:5000"
+    }
+  }
+}

+ 13 - 0
src/Robware.Api.Projects/Robware.Api.Projects.csproj

@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Robware.Projects.Gogs\Robware.Projects.Gogs.csproj" />
+    <ProjectReference Include="..\Robware.Projects\Robware.Projects.csproj" />
+  </ItemGroup>
+
+
+</Project>

+ 46 - 0
src/Robware.Api.Projects/Startup.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Net.Http;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Robware.Projects.Code;
+using Robware.Projects.Gogs;
+
+namespace Robware.Api.Projects {
+	public class Startup {
+		public Startup(IConfiguration configuration) {
+			Configuration = configuration;
+		}
+
+		public IConfiguration Configuration { get; }
+
+		// This method gets called by the runtime. Use this method to add services to the container.
+		public void ConfigureServices(IServiceCollection services) {
+			services.AddControllers();
+			services.AddHttpClient<IGitApi, GogsApi>(client => {
+				        client.BaseAddress = new Uri(Configuration["gitApiEndpoint"]);
+				        client.DefaultRequestHeaders.Add("Authorization", $"token {Configuration["gitApiToken"]}");
+			        })
+			        .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler {ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true});
+		}
+
+		// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+		public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
+			if (env.IsDevelopment()) {
+				app.UseDeveloperExceptionPage();
+			}
+
+			app.UseHttpsRedirection();
+
+			app.UseRouting();
+
+			app.UseAuthorization();
+
+			app.UseEndpoints(endpoints => {
+				endpoints.MapControllers();
+			});
+		}
+	}
+}

+ 11 - 0
src/Robware.Api.Projects/appsettings.Development.json

@@ -0,0 +1,11 @@
+{
+	"Logging": {
+		"LogLevel": {
+			"Default": "Information",
+			"Microsoft": "Warning",
+			"Microsoft.Hosting.Lifetime": "Information"
+		}
+	},
+	"gitApiEndpoint": "",
+	"gitApiToken": ""
+}

+ 12 - 0
src/Robware.Api.Projects/appsettings.json

@@ -0,0 +1,12 @@
+{
+	"Logging": {
+		"LogLevel": {
+			"Default": "Information",
+			"Microsoft": "Warning",
+			"Microsoft.Hosting.Lifetime": "Information"
+		}
+	},
+	"AllowedHosts": "*",
+	"gitApiEndpoint": "<GitApiEndpoint>",
+	"gitApiToken": "<GitApiToken>"
+}

Plik diff jest za duży
+ 114 - 0
src/Robware.Projects.Gogs.Tests/GogsApiTests.cs


+ 64 - 0
src/Robware.Projects.Gogs.Tests/HttpClientBuilder.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using RichardSzalay.MockHttp;
+
+namespace Robware.Projects.Gogs.Tests {
+	public class HttpClientBuilder {
+		private string _url, _response, _body;
+		private HttpMethod _method;
+		private readonly Dictionary<string, string> _queries = new Dictionary<string, string>();
+		private HttpStatusCode _fallbackCode = HttpStatusCode.NotImplemented;
+
+		public HttpClientBuilder WithUrl(string url) {
+			_url = url;
+			return this;
+		}
+
+		public HttpClientBuilder WithResponse(string response) {
+			_response = response;
+			return this;
+		}
+
+		public HttpClientBuilder WithMethod(HttpMethod method) {
+			_method = method;
+			return this;
+		}
+
+		public HttpClientBuilder WithQueryString(string key, string value) {
+			_queries.Add(key, value);
+			return this;
+		}
+
+		public HttpClientBuilder WithPostBody(string body) {
+			_body = body;
+			return this;
+		}
+
+		public HttpClientBuilder WithErrorStatus(HttpStatusCode statusCode) {
+			_fallbackCode = statusCode;
+			return this;
+		}
+
+		public HttpClient Build(out MockHttpMessageHandler mockHttpMessageHandler) {
+			mockHttpMessageHandler = new MockHttpMessageHandler();
+
+			mockHttpMessageHandler.Fallback.Respond(_fallbackCode, message => message.Content = new StringContent(string.Empty));
+
+			var mockedRequest = mockHttpMessageHandler.Expect(_method, _url).Respond("application/json", _response ?? "penis");
+
+			if (_queries.Any())
+				mockedRequest.WithExactQueryString(_queries);
+			if (!string.IsNullOrEmpty(_body) || _method == HttpMethod.Post)
+				mockedRequest.WithContent(_body ?? string.Empty);
+
+			var httpClient = mockHttpMessageHandler.ToHttpClient();
+			httpClient.BaseAddress = new Uri("http://example.com/");
+			return httpClient;
+		}
+
+		public HttpClient Build() => Build(out _);
+	}
+}

+ 22 - 0
src/Robware.Projects.Gogs.Tests/Robware.Projects.Gogs.Tests.csproj

@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="FluentAssertions" Version="5.10.3" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
+    <PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
+    <PackageReference Include="xunit" Version="2.4.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
+    <PackageReference Include="coverlet.collector" Version="1.0.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Robware.Projects.Gogs\Robware.Projects.Gogs.csproj" />
+  </ItemGroup>
+
+</Project>

+ 9 - 0
src/Robware.Projects.Gogs/CommitNotFoundException.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace Robware.Projects.Gogs {
+	public class CommitNotFoundException : Exception {
+		public CommitNotFoundException(string user, string repository, string hash, Exception innerException) :base ($"Could not find commit {hash} for user {user} in repository {repository}", innerException) {
+			
+		}
+	}
+}

+ 50 - 0
src/Robware.Projects.Gogs/GogsApi.cs

@@ -0,0 +1,50 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Robware.Projects.Code;
+using Robware.Projects.Gogs.Models;
+using Robware.Projects.Gogs.States;
+
+namespace Robware.Projects.Gogs {
+	public class GogsApi : IGitApi {
+		private readonly HttpClient _client;
+
+		public GogsApi(HttpClient client) {
+			_client = client;
+		}
+
+		private async Task<T> Get<T>(string url) {
+			var response = await _client.GetStringAsync(url);
+			return JsonConvert.DeserializeObject<T>(response);
+		}
+
+		public async Task<IEnumerable<Repository>> GetRepositories(string user) {
+			try {
+				return (await Get<IEnumerable<RepositoryState>>($"api/v1/users/{user}/repos")).Select(state => new GogsRepository(state));
+			}
+			catch (HttpRequestException e) {
+				throw new UserNotFoundException(user, e);
+			}
+		}
+
+		public async Task<IEnumerable<Branch>> GetBranches(string user, string repository) {
+			try {
+				return (await Get<IEnumerable<BranchState>>($"api/v1/repos/{user}/{repository}/branches")).Select(state => new GogsBranch(state));
+			}
+			catch (HttpRequestException e) {
+				throw new RepositoryNotFoundException(user, repository, e);
+			}
+		}
+
+		public async Task<Commit> GetCommit(string user, string repository, string hash) {
+			try {
+				return new GogsCommit(await Get<CommitDetailsState>($"api/v1/repos/{user}/{repository}/commits/{hash}"));
+			}
+			catch (HttpRequestException e) {
+				throw new CommitNotFoundException(user, repository, hash, e);
+			}
+		}
+	}
+}

+ 12 - 0
src/Robware.Projects.Gogs/Models/GogsBranch.cs

@@ -0,0 +1,12 @@
+using Robware.Projects.Code;
+using Robware.Projects.Gogs.States;
+
+namespace Robware.Projects.Gogs.Models {
+	public class GogsBranch : Branch {
+
+		public GogsBranch(BranchState state) {
+			Name = state.name;
+			Commit = new GogsCommit(state.commit);
+		}
+	}
+}

+ 19 - 0
src/Robware.Projects.Gogs/Models/GogsCommit.cs

@@ -0,0 +1,19 @@
+using Robware.Projects.Code;
+using Robware.Projects.Gogs.States;
+
+namespace Robware.Projects.Gogs.Models {
+	public class GogsCommit : Commit {
+		public GogsCommit(CommitState commit) {
+			Id = commit.id;
+			Message = commit.message;
+			Timestamp = commit.timestamp;
+		}
+
+		public GogsCommit(CommitDetailsState commit) {
+			Id = commit.sha;
+			Message = commit.commit.message;
+			Timestamp = commit.commit.author.date;
+			Url = commit.html_url.Replace("/commits/", "/commit/");
+		}
+	}
+}

+ 11 - 0
src/Robware.Projects.Gogs/Models/GogsRepository.cs

@@ -0,0 +1,11 @@
+using Robware.Projects.Code;
+using Robware.Projects.Gogs.States;
+
+namespace Robware.Projects.Gogs.Models {
+	public class GogsRepository : Repository {
+		public GogsRepository(RepositoryState state) {
+			Name = state.name;
+			Url = state.html_url;
+		}
+	}
+}

+ 9 - 0
src/Robware.Projects.Gogs/RepositoryNotFoundException.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace Robware.Projects.Gogs {
+	public class RepositoryNotFoundException : Exception {
+		public RepositoryNotFoundException(string user, string repository, Exception innerException) : base($"Could not find repository {repository} for user {user}", innerException) {
+			
+		}
+	}
+}

+ 19 - 0
src/Robware.Projects.Gogs/Robware.Projects.Gogs.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Folder Include="States\" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Robware.Projects\Robware.Projects.csproj" />
+  </ItemGroup>
+
+</Project>

+ 10 - 0
src/Robware.Projects.Gogs/States/AuthorState.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace Robware.Projects.Gogs.States {
+	public class AuthorState {
+		public string name { get; set; }
+		public string email { get; set; }
+		public string username { get; set; }
+		public DateTimeOffset date { get; set; }
+	}
+}

+ 7 - 0
src/Robware.Projects.Gogs/States/BranchState.cs

@@ -0,0 +1,7 @@
+namespace Robware.Projects.Gogs.States {
+	public class BranchState
+	{
+		public string name { get; set; }
+		public CommitState commit { get; set; }
+	}
+}

+ 11 - 0
src/Robware.Projects.Gogs/States/CommitDetailsState.cs

@@ -0,0 +1,11 @@
+namespace Robware.Projects.Gogs.States {
+	public class CommitDetailsState {
+		public string url { get; set; }
+		public string sha { get; set; }
+		public string html_url { get; set; }
+		public CommitState commit { get; set; }
+		public UserState author { get; set; }
+		public UserState committer { get; set; }
+		public CommitPointerState[] parents { get; set; }
+	}
+}

+ 6 - 0
src/Robware.Projects.Gogs/States/CommitPointerState.cs

@@ -0,0 +1,6 @@
+namespace Robware.Projects.Gogs.States {
+	public partial class CommitPointerState {
+		public string url { get; set; }
+		public string sha { get; set; }
+	}
+}

+ 16 - 0
src/Robware.Projects.Gogs/States/CommitState.cs

@@ -0,0 +1,16 @@
+using System;
+
+namespace Robware.Projects.Gogs.States {
+	public class CommitState {
+		public string id { get; set; }
+		public string message { get; set; }
+		public string url { get; set; }
+		public AuthorState author { get; set; }
+		public AuthorState committer { get; set; }
+		public object added { get; set; }
+		public object removed { get; set; }
+		public object modified { get; set; }
+		public DateTimeOffset timestamp { get; set; }
+		public CommitPointerState tree { get; set; }
+	}
+}

+ 10 - 0
src/Robware.Projects.Gogs/States/RepositoryOwnerState.cs

@@ -0,0 +1,10 @@
+namespace Robware.Projects.Gogs.States {
+	public class RepositoryOwnerState {
+		public int id { get; set; }
+		public string username { get; set; }
+		public string login { get; set; }
+		public string full_name { get; set; }
+		public string email { get; set; }
+		public string avatar_url { get; set; }
+	}
+}

+ 7 - 0
src/Robware.Projects.Gogs/States/RepositoryPermissionsState.cs

@@ -0,0 +1,7 @@
+namespace Robware.Projects.Gogs.States {
+	public class RepositoryPermissionsState {
+		public bool admin { get; set; }
+		public bool push { get; set; }
+		public bool pull { get; set; }
+	}
+}

+ 31 - 0
src/Robware.Projects.Gogs/States/RepositoryState.cs

@@ -0,0 +1,31 @@
+using System;
+
+namespace Robware.Projects.Gogs.States {
+	public class RepositoryState {
+		public int id { get; set; }
+		public RepositoryOwnerState owner { get; set; }
+		public string name { get; set; }
+		public string full_name { get; set; }
+		public string description { get; set; }
+		public bool _private { get; set; }
+		public bool fork { get; set; }
+		public object parent { get; set; }
+		public bool empty { get; set; }
+		public bool mirror { get; set; }
+		public int size { get; set; }
+		public string html_url { get; set; }
+		public string ssh_url { get; set; }
+		public string clone_url { get; set; }
+		public string website { get; set; }
+		public int stars_count { get; set; }
+		public int forks_count { get; set; }
+		public int watchers_count { get; set; }
+		public int open_issues_count { get; set; }
+		public string default_branch { get; set; }
+		public DateTime created_at { get; set; }
+		public DateTime updated_at { get; set; }
+		public RepositoryPermissionsState permissions { get; set; }
+	}
+}
+
+

+ 10 - 0
src/Robware.Projects.Gogs/States/UserState.cs

@@ -0,0 +1,10 @@
+namespace Robware.Projects.Gogs.States {
+	public partial class UserState {
+		public long id { get; set; }
+		public string username { get; set; }
+		public string login { get; set; }
+		public string full_name { get; set; }
+		public string email { get; set; }
+		public string avatar_url { get; set; }
+	}
+}

+ 9 - 0
src/Robware.Projects.Gogs/UserNotFoundException.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace Robware.Projects.Gogs {
+	public class UserNotFoundException : Exception {
+		public UserNotFoundException(string user, Exception innerException) : base($"Could not find user {user}", innerException) {
+			
+		}
+	}
+}

+ 6 - 0
src/Robware.Projects/Code/Branch.cs

@@ -0,0 +1,6 @@
+namespace Robware.Projects.Code {
+	public abstract class Branch {
+		public string Name { get; protected set; }
+		public Commit Commit { get; protected set; }
+	}
+}

+ 10 - 0
src/Robware.Projects/Code/Commit.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace Robware.Projects.Code {
+	public abstract class Commit {
+		public string Id { get; protected set; }
+		public string Message { get; protected set; }
+		public DateTimeOffset Timestamp { get; protected set; }
+		public string Url { get; protected set; }
+	}
+}

+ 10 - 0
src/Robware.Projects/Code/IGitApi.cs

@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Robware.Projects.Code {
+	public interface IGitApi {
+		Task<IEnumerable<Repository>> GetRepositories(string user);
+		Task<IEnumerable<Branch>> GetBranches(string user, string repository);
+		Task<Commit> GetCommit(string user, string repository, string hash);
+	}
+}

+ 6 - 0
src/Robware.Projects/Code/Repository.cs

@@ -0,0 +1,6 @@
+namespace Robware.Projects.Code {
+	public abstract class Repository {
+		public string Name { get; protected set; }
+		public string Url { get; protected set; }
+	}
+}

+ 7 - 0
src/Robware.Projects/Robware.Projects.csproj

@@ -0,0 +1,7 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+  </PropertyGroup>
+
+</Project>