| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | # One repository import, from queued through to done or failed. Drives the |
| 4 | # progress UI and makes a failed import retryable. The actual clone/push runs in |
| 5 | # RepositoryImportJob; this record is the state it advances. |
| 6 | class RepositoryImport < ApplicationRecord |
| 7 | belongs_to :user |
| 8 | belongs_to :repository, optional: true |
| 9 | |
| 10 | MODES = %w[migrate mirror].freeze |
| 11 | |
| 12 | # Ordered lifecycle. `cloning`/`pushing`/`fetching_lfs` are the long phases the |
| 13 | # UI narrates; terminal states are `done` and `failed`. |
| 14 | STATUSES = %w[queued cloning pushing fetching_lfs finalizing done failed].freeze |
| 15 | TERMINAL = %w[done failed].freeze |
| 16 | ACTIVE = STATUSES - TERMINAL |
| 17 | |
| 18 | validates :mode, inclusion: { in: MODES } |
| 19 | validates :status, inclusion: { in: STATUSES } |
| 20 | validates :source_url, presence: true |
| 21 | validates :target_name, presence: true |
| 22 | |
| 23 | scope :active, -> { where(status: ACTIVE) } |
| 24 | scope :recent, -> { order(created_at: :desc) } |
| 25 | |
| 26 | def mirror? |
| 27 | mode == "mirror" |
| 28 | end |
| 29 | |
| 30 | def migrate? |
| 31 | mode == "migrate" |
| 32 | end |
| 33 | |
| 34 | def terminal? |
| 35 | TERMINAL.include?(status) |
| 36 | end |
| 37 | |
| 38 | def failed? |
| 39 | status == "failed" |
| 40 | end |
| 41 | |
| 42 | def done? |
| 43 | status == "done" |
| 44 | end |
| 45 | |
| 46 | # A retry is allowed only from a failed state; re-running from any active or |
| 47 | # done state would risk duplicate work against a live import. |
| 48 | def retryable? |
| 49 | failed? |
| 50 | end |
| 51 | |
| 52 | # Human-friendly one-liner for the progress UI. |
| 53 | def status_label |
| 54 | { |
| 55 | "queued" => "Queued", |
| 56 | "cloning" => "Cloning source", |
| 57 | "pushing" => "Pushing to siGit", |
| 58 | "fetching_lfs" => "Fetching LFS objects", |
| 59 | "finalizing" => "Finalizing", |
| 60 | "done" => "Imported", |
| 61 | "failed" => "Failed" |
| 62 | }.fetch(status, status.humanize) |
| 63 | end |
| 64 | |
| 65 | # Advance the state machine, stamping timing on entry/exit. Kept tiny so the |
| 66 | # job body reads as a sequence of `transition_to(...)` calls. |
| 67 | def transition_to(new_status, error: nil) |
| 68 | attrs = { status: new_status } |
| 69 | attrs[:started_at] = Time.current if new_status == "cloning" && started_at.nil? |
| 70 | attrs[:finished_at] = Time.current if TERMINAL.include?(new_status) |
| 71 | attrs[:error_message] = error if error |
| 72 | update!(attrs) |
| 73 | end |
| 74 | end |