import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import { RefreshCw } from "lucide-react"; import type { GitSyncStatus } from "#/types/git-sync"; import { formatTimeDelta } from "#/utils/format-time-delta"; import { parseGitRemoteUrl } from "#/utils/parse-git-remote-url"; import { cn } from "#/utils/utils"; import GitBranchIcon from "#/icons/git-branch.svg?react"; import GlobeIcon from "#/icons/globe.svg?react"; import FolderIcon from "#/icons/folder.svg?react"; import ClockIcon from "#/icons/clock.svg?react"; import { SectionCard } from "#/components/features/automations/detail/section-card"; import { ConfigField } from "#/components/features/automations/detail/config-field"; import { BrandButton } from "#/components/features/settings/brand-button"; import { GitSyncStatusPill } from "./git-sync-status-pill"; import { GitSyncActivityRow, type GitSyncActivityState, } from "./git-sync-activity-row"; /** * A browsable URL for the configured remote, or `null` when there isn't one. * A bare repo on disk is a valid sync target, so a local path -- and anything * else that doesn't parse -- stays plain text rather than becoming a dead link. * * An http(s) remote is already browsable, so it is reused as-is apart from the * `.git` suffix and any embedded credentials, which keeps forge-specific paths * (Azure's `org/project/_git/repo`) and non-default ports intact. Other schemes * -- `ssh://`, `git://`, `git@host:owner/repo` -- carry no browsable form, so * they are rebuilt over https from the parsed host and repository. */ function browseUrlFor(repoUrl: string): string | null { const parsed = parseGitRemoteUrl(repoUrl); if (!parsed?.host || !parsed.repository) return null; if (/^https?:\/\//i.test(parsed.url)) { const url = new URL(parsed.url); url.username = ""; url.password = ""; url.pathname = url.pathname.replace(/\.git$/, ""); return url.toString(); } return `https://${parsed.host}/${parsed.repository}`; } interface GitSyncOverviewSectionProps { status: GitSyncStatus; onSyncNow: () => void; isSyncing: boolean; syncActivity: GitSyncActivityState; syncStartedAt: string | null; canManage: boolean; } export function GitSyncOverviewSection({ status, onSyncNow, isSyncing, syncActivity, syncStartedAt, canManage, }: GitSyncOverviewSectionProps) { const { t } = useTranslation("openhands"); const repoHref = browseUrlFor(status.repo_url); return ( } title={t(I18nKey.AUTOMATIONS$GIT_SYNC$STATUS_TITLE)} >
} > {t( isSyncing ? I18nKey.AUTOMATIONS$GIT_SYNC$SYNCING : I18nKey.AUTOMATIONS$GIT_SYNC$SYNC_NOW, )}
} label={t(I18nKey.AUTOMATIONS$GIT_SYNC$REPOSITORY)} > {repoHref ? ( {status.repo_url} ) : ( {status.repo_url || t(I18nKey.AUTOMATIONS$GIT_SYNC$NOT_CONFIGURED)} )} } label={t(I18nKey.AUTOMATIONS$GIT_SYNC$FIELD_BRANCH)} > {status.branch} } label={t(I18nKey.AUTOMATIONS$GIT_SYNC$FIELD_PATH)} > {status.path} } label={t(I18nKey.AUTOMATIONS$GIT_SYNC$LAST_SYNCED_COMMIT)} > {status.last_synced_commit ? ( {status.last_synced_commit.slice(0, 7)} ) : ( t(I18nKey.AUTOMATIONS$GIT_SYNC$NEVER_SYNCED) )} } label={t(I18nKey.AUTOMATIONS$GIT_SYNC$LAST_SYNCED_AT)} > {status.last_synced_at ? `${formatTimeDelta(status.last_synced_at)} ${t(I18nKey.CONVERSATION$AGO)}` : t(I18nKey.AUTOMATIONS$GIT_SYNC$NEVER_SYNCED)} } label={t(I18nKey.AUTOMATIONS$GIT_SYNC$FIELD_INTERVAL)} > {status.interval_seconds > 0 ? t(I18nKey.AUTOMATIONS$GIT_SYNC$EVERY_N_SECONDS, { count: status.interval_seconds, }) : t(I18nKey.AUTOMATIONS$GIT_SYNC$MANUAL_ONLY)} } label={t(I18nKey.AUTOMATIONS$GIT_SYNC$PENDING_CHANGES)} > 0 && "text-[var(--oh-warning)]")} > {status.dirty_count}
); }