Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions app/controllers/my/project_repo_mappings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ class My::ProjectRepoMappingsController < InertiaController
layout "inertia", only: [ :index, :show ]

before_action :ensure_current_user
before_action :require_github_oauth, only: [ :edit, :update ]
before_action :set_project_repo_mapping_for_edit, only: [ :edit, :update ]
before_action :require_github_oauth, only: [ :update ]
before_action :set_project_repo_mapping_for_edit, only: [ :update ]
before_action :set_project_repo_mapping, only: [ :archive, :unarchive, :toggle_share ]

def index
Expand All @@ -24,16 +24,18 @@ def index
}
end

def edit; end

def update
@project_repo_mapping.project_name = params[:project_name] if @project_repo_mapping.new_record?

if @project_repo_mapping.update(project_repo_mapping_params)
redirect_to my_projects_path, notice: "Repository mapping updated successfully."
else
flash.now[:alert] = @project_repo_mapping.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity
redirect_back fallback_location: my_projects_path,
inertia: { errors: {
repo_url: @project_repo_mapping.errors[:repo_url].to_sentence,
repo_url_project_name: @project_repo_mapping.project_name,
repo_url_value: @project_repo_mapping.repo_url
} }
end
end

Expand Down
17 changes: 17 additions & 0 deletions app/javascript/pages/Projects/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
to = "",
total_projects,
projects_data,
errors = {},
}: {
page_title: string;
show_archived: boolean;
Expand All @@ -35,6 +36,11 @@
has_activity: boolean;
projects: ProjectCardType[];
};
errors?: {
repo_url?: string;
repo_url_project_name?: string;
repo_url_value?: string;
};
} = $props();

const indexPath = myProjectRepoMappings.index.path();
Expand Down Expand Up @@ -63,6 +69,13 @@
confirmLabel: string;
} | null>(null);

$effect(() => {
if (errors.repo_url && errors.repo_url_project_name) {
editingProjectKey = errors.repo_url_project_name;
repoUrlDraft = errors.repo_url_value || "";
}
});

const skeletonCount = $derived(
Math.min(
Math.max(Number.isFinite(total_projects) ? total_projects : 0, 4),
Expand Down Expand Up @@ -349,6 +362,10 @@
onArchive={openStatusChangeModal}
onShowBrokenInfo={() => (brokenNameModalOpen = true)}
editing={editingProjectKey === project.project_key}
repoUrlError={errors.repo_url_project_name ===
project.project_key
? errors.repo_url
: undefined}
bind:repoUrlDraft
onCancelEdit={closeMappingEditor}
/>
Expand Down
6 changes: 6 additions & 0 deletions app/javascript/pages/Projects/components/ProjectCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
onArchive,
onShowBrokenInfo,
editing,
repoUrlError,
repoUrlDraft = $bindable(""),
onCancelEdit,
}: {
Expand All @@ -29,6 +30,7 @@
onArchive: (project: ProjectCard, restoring: boolean) => void;
onShowBrokenInfo: () => void;
editing: boolean;
repoUrlError?: string;
repoUrlDraft?: string;
onCancelEdit: () => void;
} = $props();
Expand Down Expand Up @@ -189,6 +191,7 @@
action={updatePath}
method="patch"
class="relative z-20 mt-4 space-y-3 border-t border-surface-200/40 pt-4"
onSuccess={onCancelEdit}
>
<input
type="url"
Expand All @@ -197,6 +200,9 @@
placeholder="https://github.com/owner/repo"
class="w-full rounded-lg border border-surface-200 bg-input px-3 py-2 text-sm text-surface-content focus:border-primary focus:outline-none"
/>
{#if repoUrlError}
<p class="text-sm text-red">{repoUrlError}</p>
{/if}
<div class="flex gap-2">
<Button type="submit" variant="primary" size="sm" class="flex-1"
>Save</Button
Expand Down
11 changes: 9 additions & 2 deletions app/models/project_repo_mapping.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ProjectRepoMapping < ApplicationRecord
message: "must be a valid repository URL"
}, if: :repo_url_required?
validate :repo_host_supported, if: :repo_url_required?
validate :repo_url_exists, if: :repo_url_required?
validate :repo_url_exists, if: :repo_url_verification_required?

scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
Expand Down Expand Up @@ -42,8 +42,15 @@ def repo_host_supported
end
end

def repo_url_verification_required?
repo_url_required? && (new_record? || will_save_change_to_repo_url?)
end

def repo_url_exists
errors.add(:repo_url, "is not cloneable") unless GitRemote.check_remote_exists(repo_url)
return if errors[:repo_url].any?

exists = RepoHost::ServiceFactory.for_url(user, repo_url).repository_exists?
errors.add(:repo_url, "does not exist or is not accessible") if exists == false
end

def create_repository_and_sync
Expand Down
22 changes: 22 additions & 0 deletions app/services/repo_host/github_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ def self.api_headers_for(access_token)
}
end

def repository_exists?
return nil unless user.github_access_token.present?

response = self.class.api_client(user.github_access_token)
.get("https://api.github.com/repos/#{owner}/#{repo}")

case response.status.code
when 200 then true
when 404 then github_repo_scope?(response) ? false : nil
else
Rails.logger.warn "[#{self.class.name}] Could not verify #{owner}/#{repo}: #{response.status}"
nil
end
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
report_error(e, message: "[#{self.class.name}] Error verifying #{owner}/#{repo}")
nil
end

def fetch_repo_metadata
return nil unless user.github_access_token.present?

Expand All @@ -41,6 +59,10 @@ def fetch_repo_metadata

private

def github_repo_scope?(response)
response.headers["X-OAuth-Scopes"].to_s.split(",").map(&:strip).include?("repo")
end

def api_headers
self.class.api_headers_for(user.github_access_token)
end
Expand Down
36 changes: 0 additions & 36 deletions app/views/my/project_repo_mappings/edit.html.erb

This file was deleted.

1 change: 0 additions & 1 deletion config/initializers/js_from_routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ module JsFromRoutes

my_projects
my_project
edit_my_project_repo_mapping
my_project_repo_mapping
archive_my_project_repo_mapping
unarchive_my_project_repo_mapping
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def matches?(request)
get :wakatime_download_link, on: :collection
end

resources :project_repo_mappings, param: :project_name, only: [ :edit, :update ], constraints: { project_name: /.+/ } do
resources :project_repo_mappings, param: :project_name, only: [ :update ], constraints: { project_name: /.+/ } do
member do
patch :archive
patch :unarchive
Expand Down
14 changes: 0 additions & 14 deletions lib/git_remote.rb

This file was deleted.

15 changes: 15 additions & 0 deletions test/controllers/my/project_repo_mappings_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ class My::ProjectRepoMappingsControllerTest < ActionDispatch::IntegrationTest
assert_predicate mapping.reload, :archived?
end

test "update returns validation errors to the projects page" do
user = User.create!(timezone: "UTC", github_uid: "123")
mapping = user.project_repo_mappings.create!(project_name: "alpha")

sign_in_as(user)
patch my_project_repo_mapping_path(project_name: mapping.project_name),
params: { project_repo_mapping: { repo_url: "https://example.com/owner/repo" } },
headers: { "HTTP_REFERER" => my_projects_url(show_archived: true) }

assert_redirected_to my_projects_path(show_archived: true)
assert_includes session[:inertia_errors][:repo_url], "We only support GitHub repositories"
assert_equal mapping.project_name, session[:inertia_errors][:repo_url_project_name]
assert_equal "https://example.com/owner/repo", session[:inertia_errors][:repo_url_value]
end

private

def create_project_heartbeats(user, project_name)
Expand Down
80 changes: 80 additions & 0 deletions test/models/project_repo_mapping_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,84 @@ class ProjectRepoMappingTest < ActiveSupport::TestCase
assert_not duplicate.valid?
assert_includes duplicate.errors[:project_name], "has already been taken"
end

test "existing GitHub repository URLs are valid" do
user = User.create!(github_access_token: "github-token")
stub_request(:get, "https://api.github.com/repos/yousseftechdev/RoboEyesMacroPad")
.to_return(status: 200, body: "{}")
mapping = user.project_repo_mappings.build(
project_name: "macro-pad",
repo_url: "https://github.com/yousseftechdev/RoboEyesMacroPad"
)

assert_predicate mapping, :valid?
end

test "inaccessible GitHub repository URLs are invalid" do
user = User.create!(github_access_token: "github-token")
stub_request(:get, "https://api.github.com/repos/example/missing")
.to_return(
status: 404,
body: '{"message":"Not Found"}',
headers: { "X-OAuth-Scopes" => "repo, user:email" }
)
mapping = user.project_repo_mappings.build(
project_name: "missing",
repo_url: "https://github.com/example/missing"
)

assert_not mapping.valid?
assert_includes mapping.errors[:repo_url], "does not exist or is not accessible"
end

test "a private repository hidden from a limited token is not treated as nonexistent" do
user = User.create!(github_access_token: "github-token")
stub_request(:get, "https://api.github.com/repos/example/private")
.to_return(
status: 404,
body: '{"message":"Not Found"}',
headers: { "X-OAuth-Scopes" => "user:email" }
)
mapping = user.project_repo_mappings.build(
project_name: "private",
repo_url: "https://github.com/example/private"
)

assert_predicate mapping, :valid?
end

test "temporary GitHub failures do not mark repository URLs as nonexistent" do
user = User.create!(github_access_token: "github-token")
stub_request(:get, "https://api.github.com/repos/example/repository")
.to_return(status: 503, body: '{"message":"Service unavailable"}')
mapping = user.project_repo_mappings.build(
project_name: "repository",
repo_url: "https://github.com/example/repository"
)

assert_predicate mapping, :valid?
end

test "TLS failures do not mark repository URLs as nonexistent" do
user = User.create!(github_access_token: "github-token")
stub_request(:get, "https://api.github.com/repos/example/repository")
.to_raise(OpenSSL::SSL::SSLError.new("certificate verify failed"))
mapping = user.project_repo_mappings.build(
project_name: "repository",
repo_url: "https://github.com/example/repository"
)

assert_predicate mapping, :valid?
end

test "unchanged repository URLs are not remotely verified" do
user = User.create!(github_access_token: "github-token")
mapping = user.project_repo_mappings.create!(project_name: "repository")
mapping.update_column(:repo_url, "https://github.com/example/repository")

mapping.archive!

assert_predicate mapping.reload, :archived?
assert_not_requested :get, "https://api.github.com/repos/example/repository"
end
end