~edwargix/git.sr.ht

4e8fd8c296e22cba0e198250e8a80c26908cf6ec — Adnan Maolood 2 years ago c372753
api: Handle clone errors

This adds two new columns to the database: clone_status and clone_error,
which are used to store the clone status as well as any error that
occurs. The error is then displayed to the user in the frontend.
M api/graph/resolver.go => api/graph/resolver.go +9 -0
@@ 15,3 15,12 @@ var allowedCloneSchemes = map[string]struct{}{
	"http":  struct{}{},
	"git":   struct{}{},
}

type CloneStatus string

const (
	CloneNone       CloneStatus = "NONE"
	CloneInProgress CloneStatus = "IN_PROGRESS"
	CloneComplete   CloneStatus = "COMPLETE"
	CloneError      CloneStatus = "ERROR"
)

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +7 -4
@@ 128,12 128,15 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi
			panic(fmt.Errorf("Unknown visibility %s", visibility)) // Invariant
		}

		cloneInProgress := cloneURL != nil
		cloneStatus := CloneNone
		if cloneURL != nil {
			cloneStatus = CloneInProgress
		}

		row := tx.QueryRowContext(ctx, `
			INSERT INTO repository (
				created, updated, name, description, path, visibility, owner_id,
				clone_in_progress
				clone_status
			) VALUES (
				NOW() at time zone 'utc',
				NOW() at time zone 'utc',


@@ 141,7 144,7 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi
			) RETURNING 
				id, created, updated, name, description, visibility,
				path, owner_id;
		`, name, description, repoPath, dvis, user.UserID, cloneInProgress)
		`, name, description, repoPath, dvis, user.UserID, cloneStatus)
		if err := row.Scan(&repo.ID, &repo.Created, &repo.Updated, &repo.Name,
			&repo.Description, &repo.RawVisibility,
			&repo.Path, &repo.OwnerID); err != nil {


@@ 188,7 191,7 @@ func (r *mutationResolver) CreateRepository(ctx context.Context, name string, vi
			}
		}

		if cloneInProgress {
		if cloneURL != nil {
			u, err := url.Parse(*cloneURL)
			if err != nil {
				return valid.Errorf(ctx, "cloneUrl", "Invalid clone URL: %s", err)

M api/repos/middleware.go => api/repos/middleware.go +25 -20
@@ 33,6 33,15 @@ func Middleware(queue *work.Queue) func(next http.Handler) http.Handler {
	}
}

type CloneStatus string

const (
	CloneNone       CloneStatus = "NONE"
	CloneInProgress CloneStatus = "IN_PROGRESS"
	CloneComplete   CloneStatus = "COMPLETE"
	CloneError      CloneStatus = "ERROR"
)

// Schedules a clone.
func Clone(ctx context.Context, repoID int, repo *git.Repository, cloneURL string) {
	queue, ok := ctx.Value(ctxKey).(*work.Queue)


@@ 40,33 49,29 @@ func Clone(ctx context.Context, repoID int, repo *git.Repository, cloneURL strin
		panic("No repos worker for this context")
	}
	task := work.NewTask(func(ctx context.Context) error {
		defer func() {
			recovered := recover()
			err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
				_, err := tx.Exec(
					`UPDATE repository SET clone_in_progress = false WHERE id = $1;`,
					repoID,
				)
				if err != nil {
					return err
				}
				return nil
			})
			if err != nil {
				panic(err)
			}
			if recovered != nil {
				panic(recovered)
			}
		}()
		cloneCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
		defer cancel()
		err := repo.Clone(cloneCtx, &git.CloneOptions{
			URL:               cloneURL,
			RecurseSubmodules: git.NoRecurseSubmodules,
		})
		cloneStatus := CloneComplete
		var cloneError sql.NullString
		if err != nil {
			// TODO: Set repo to error state. Email error to user.
			cloneStatus = CloneError
			cloneError.String = err.Error()
			cloneError.Valid = true
		}
		if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
			_, err := tx.Exec(`
				UPDATE repository
				SET clone_status = $2, clone_error = $3
				WHERE id = $1;`, repoID, cloneStatus, cloneError)
			if err != nil {
				return err
			}
			return nil
		}); err != nil {
			panic(err)
		}
		return nil

A gitsrht/alembic/versions/0a3d114e8a18_add_clone_status_to_repository.py => gitsrht/alembic/versions/0a3d114e8a18_add_clone_status_to_repository.py +49 -0
@@ 0,0 1,49 @@
"""Add clone_status to repository

Revision ID: 0a3d114e8a18
Revises: 163a7732e6a0
Create Date: 2022-02-15 08:25:12.564021

"""

# revision identifiers, used by Alembic.
revision = '0a3d114e8a18'
down_revision = '163a7732e6a0'

from alembic import op
import sqlalchemy as sa


def upgrade():
    op.execute("""
    CREATE TYPE clone_status AS ENUM (
        'NONE',
        'IN_PROGRESS',
        'COMPLETE',
        'ERROR'
    );

    ALTER TABLE repository
    ADD COLUMN clone_status clone_status NOT NULL DEFAULT 'NONE';
    ALTER TABLE repository
    ALTER COLUMN clone_status DROP DEFAULT;
    ALTER TABLE repository
    ADD COLUMN clone_error varchar
    CHECK ((clone_status = 'ERROR') != (clone_error IS NULL));
    ALTER TABLE repository
    DROP COLUMN clone_in_progress;
    """)


def downgrade():
    op.execute("""
    ALTER TABLE repository
    DROP COLUMN clone_error;
    ALTER TABLE repository
    DROP COLUMN clone_status;
    DROP TYPE clone_status;
    ALTER TABLE repository
    ADD COLUMN clone_in_progress boolean NOT NULL DEFAULT false;
    ALTER TABLE repository
    ALTER COLUMN clone_in_progress DROP DEFAULT;
    """)

M gitsrht/templates/empty-repo.html => gitsrht/templates/empty-repo.html +7 -1
@@ 1,6 1,8 @@
{% extends "repo.html" %}
{% block repohead %}
{% if repo.clone_status == 'IN_PROGRESS' %}
<meta http-equiv="refresh" content="5">
{% endif %}
{% endblock %}

{% block content %}


@@ 23,10 25,14 @@
      </dl>
    </div>
    <div class="col-md-8">
      {% if repo.clone_in_progress %}
      {% if repo.clone_status == 'IN_PROGRESS' %}
      <div class="alert alert-primary">
        A clone operation is currently in progress.
      </div>
      {% elif repo.clone_status == 'ERROR' %}
      <div class="alert alert-danger">
        An error occured while cloning: {{repo.clone_error}}
      </div>
      {% endif %}
      <p>
        Nothing here yet!

M gitsrht/types/__init__.py => gitsrht/types/__init__.py +4 -1
@@ 3,6 3,7 @@ import sqlalchemy as sa
from sqlalchemy import event
import sqlalchemy_utils as sau
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.dialects import postgresql
from srht.database import Base
from srht.oauth import ExternalUserMixin, ExternalOAuthTokenMixin
from gitsrht.git import Repository as GitRepository


@@ 45,7 46,9 @@ class Repository(Base):
            nullable=False,
            default=RepoVisibility.public)
    readme = sa.Column(sa.Unicode)
    clone_in_progress = sa.Column(sa.Boolean, nullable=False)
    clone_status = sa.Column(postgresql.ENUM(
        'NONE', 'IN_PROGRESS', 'COMPLETE', 'ERROR'), nullable=False)
    clone_error = sa.Column(sa.Unicode)

    @declared_attr
    def owner_id(cls):