mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 19:46:44 +03:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b604cad2fd | ||
|
|
983b47fa76 | ||
|
|
ea065d22a5 | ||
|
|
577d198e1a | ||
|
|
9ead32e9e6 | ||
|
|
993e7c1bc0 | ||
|
|
5b275a17e3 | ||
|
|
0caa327142 | ||
|
|
00d516a59d | ||
|
|
d68b822c76 | ||
|
|
b660d2dd9a | ||
|
|
4d8d75c8a6 | ||
|
|
e6c4ca0731 | ||
|
|
f03405b87a | ||
|
|
ce367e3761 | ||
|
|
cfe65cadca | ||
|
|
68108b8383 | ||
|
|
d2bec1d56e | ||
|
|
c9404f2674 | ||
|
|
80ef19c634 | ||
|
|
83c924566c | ||
|
|
7b58df375e | ||
|
|
1d27bd31d8 | ||
|
|
13d4257c4f | ||
|
|
818ba77693 | ||
|
|
056970e577 | ||
|
|
65ea73e238 | ||
|
|
398f00aceb | ||
|
|
50972713a3 | ||
|
|
fbf3033455 | ||
|
|
cc4d8dabbc | ||
|
|
8bba3d3521 | ||
|
|
be63555e5c | ||
|
|
32a906369f | ||
|
|
064474fd13 | ||
|
|
2ac933b599 | ||
|
|
403fe08bae | ||
|
|
23c7ff7349 | ||
|
|
3169af44cb | ||
|
|
c1d93dbbc6 | ||
|
|
047719cde9 | ||
|
|
13d4b03541 | ||
|
|
f07ae220b0 | ||
|
|
01647445f2 | ||
|
|
13cbf86309 | ||
|
|
792096d209 | ||
|
|
e94de5c9ca | ||
|
|
b3f42624d8 | ||
|
|
d79e4fecf4 | ||
|
|
eb78f959c7 | ||
|
|
51e536c317 | ||
|
|
7af1f6da17 | ||
|
|
c7e310b340 | ||
|
|
23cfa45d89 | ||
|
|
b1346e8c77 | ||
|
|
6e673249dc |
@@ -1,3 +1,4 @@
|
||||
Evaluate all the updates being made.
|
||||
Make sure the user has clarified if its a major, minor or a patch release.
|
||||
Update the package.json first before you push the tag.
|
||||
Update CHANGELOG.md
|
||||
Use the chnages in the git log to determine if its a major, minor or a patch release.
|
||||
Update the package.json first before you push the tag.
|
||||
|
||||
BIN
.github/assets/configuration.png
vendored
BIN
.github/assets/configuration.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 945 KiB After Width: | Height: | Size: 891 KiB |
133
CHANGELOG.md
133
CHANGELOG.md
@@ -5,6 +5,139 @@ All notable changes to the Gitea Mirror project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.20.0] - 2025-07-07
|
||||
|
||||
### Changed
|
||||
- **BREAKING**: Repository moved from `arunavo4/gitea-mirror` to `RayLabsHQ/gitea-mirror`
|
||||
- Docker images now hosted at `ghcr.io/raylabshq/gitea-mirror`
|
||||
- Updated all repository references and links to new organization
|
||||
- License changed from MIT to GNU General Public License v3.0
|
||||
|
||||
### Fixed
|
||||
- Updated GitHub API endpoint for version checking to use new repository location
|
||||
- Corrected all documentation references to point to RayLabsHQ organization
|
||||
|
||||
### Security
|
||||
- Removed test security script after confirming vulnerability resolution
|
||||
- Updated base Docker image to version 1.2.18-alpine
|
||||
|
||||
### Documentation
|
||||
- Added repository migration notice in README
|
||||
- Updated quickstart guide with new repository URLs
|
||||
- Updated LXC deployment documentation with new repository location
|
||||
|
||||
## [2.18.0] - 2025-06-24
|
||||
|
||||
### Added
|
||||
- Fourth organization strategy "Mixed Mode" that combines aspects of existing strategies
|
||||
- Personal repositories go to a single configurable organization
|
||||
- Organization repositories preserve their GitHub organization structure
|
||||
- "Override Options" info button in Organization Strategy component explaining customization features
|
||||
- Organization overrides via edit buttons on organization cards
|
||||
- Repository overrides via inline destination editor
|
||||
- Starred repositories behavior and priority hierarchy
|
||||
|
||||
### Improved
|
||||
- Simplified mixed strategy implementation to reuse existing database fields
|
||||
- Enhanced organization strategy UI with comprehensive override documentation
|
||||
- Better visual indicators for the new mixed strategy with orange color theme
|
||||
|
||||
## [2.17.0] - 2025-06-24
|
||||
|
||||
### Added
|
||||
- Custom destination control for individual repositories with inline editing
|
||||
- Organization-level destination overrides with visual destination editor
|
||||
- Personal repositories organization override configuration option
|
||||
- Visual indicators for starred repositories (⭐ icon) in repository list
|
||||
- Repository-level destination override API endpoint
|
||||
- Destination customization priority hierarchy system
|
||||
- "View on Gitea" buttons for organizations with smart tooltip states
|
||||
|
||||
### Changed
|
||||
- Enhanced repository table with destination column showing both GitHub org and Gitea destination
|
||||
- Updated organization cards to display custom destinations with visual indicators
|
||||
- Improved getGiteaRepoOwnerAsync to support repository-level destination overrides
|
||||
|
||||
### Improved
|
||||
- Better visual feedback for custom destinations with badges and inline editing
|
||||
- Enhanced user experience with hover-based edit buttons
|
||||
- Comprehensive destination customization documentation in README
|
||||
|
||||
## [2.16.3] - 2025-06-20
|
||||
|
||||
### Added
|
||||
- Custom 404 error page with helpful navigation links
|
||||
- HoverCard components for better UX in configuration forms
|
||||
|
||||
### Improved
|
||||
- Replaced popover components with hover cards for information tooltips
|
||||
- Enhanced user experience with responsive hover interactions
|
||||
|
||||
## [2.16.2] - 2025-06-17
|
||||
|
||||
### Added
|
||||
- Bulk actions for repository management with selection support
|
||||
|
||||
### Improved
|
||||
- Enhanced organization card display with status badges and improved layout
|
||||
|
||||
## [2.16.1] - 2025-06-17
|
||||
|
||||
### Improved
|
||||
- Improved repository owner handling and mirror strategy in Gitea integration
|
||||
- Updated label for starred repositories organization for consistency
|
||||
|
||||
## [2.16.0] - 2025-06-17
|
||||
|
||||
### Added
|
||||
- Enhanced OrganizationConfiguration component with improved layout and metadata options
|
||||
- New GitHubMirrorSettings component with better organization and flexibility
|
||||
- Enhanced starred repositories content selection and improved layout
|
||||
|
||||
### Improved
|
||||
- Enhanced configuration interface layout and spacing across multiple components
|
||||
- Streamlined OrganizationStrategy component with cleaner imports and better organization
|
||||
- Improved responsive layout for larger screens in configuration forms
|
||||
- Better icon usage and clarity in configuration components
|
||||
- Enhanced tooltip descriptions and component organization
|
||||
- Improved version comparison logic in health API
|
||||
- Enhanced issue mirroring logic for starred repositories
|
||||
|
||||
### Fixed
|
||||
- Fixed mirror to single organization functionality
|
||||
- Resolved organization strategy layout issues
|
||||
- Cleaned up unused imports across multiple components
|
||||
|
||||
### Refactored
|
||||
- Simplified component structures by removing unused imports and dependencies
|
||||
- Enhanced layout flexibility in GitHubConfigForm and GiteaConfigForm components
|
||||
- Improved component organization and code clarity
|
||||
- Removed ConnectionsForm and useMirror hook for better code organization
|
||||
|
||||
## [2.14.0] - 2025-06-17
|
||||
|
||||
### Added
|
||||
- Enhanced UI components with @radix-ui/react-accordion dependency for improved configuration interface
|
||||
|
||||
### Fixed
|
||||
- Mirror strategies now properly route repositories based on selected strategy
|
||||
- Starred repositories now correctly go to the designated starred repos organization
|
||||
- Organization routing for single-org and flat-user strategies
|
||||
|
||||
### Improved
|
||||
- Documentation now explains all three mirror strategies (preserve, single-org, flat-user)
|
||||
- Added detailed mirror strategy configuration guide
|
||||
- Updated CLAUDE.md with mirror strategy architecture information
|
||||
- Enhanced Docker Compose development configuration
|
||||
|
||||
## [2.13.2] - 2025-06-15
|
||||
|
||||
### Improved
|
||||
- Enhanced documentation design and layout
|
||||
- Updated README with improved formatting and content
|
||||
|
||||
## [2.13.1] - 2025-06-15
|
||||
|
||||
### Added
|
||||
|
||||
@@ -79,6 +79,13 @@ export async function POST({ request }: APIContext) {
|
||||
- Tracks status in database
|
||||
- Supports scheduled automatic mirroring
|
||||
|
||||
6. **Mirror Strategies**: Three ways to organize repositories in Gitea:
|
||||
- **preserve**: Maintains GitHub structure (default)
|
||||
- **single-org**: All repos go to one organization
|
||||
- **flat-user**: All repos go under user account
|
||||
- Starred repos always go to separate organization (starredReposOrg)
|
||||
- Routing logic in `getGiteaRepoOwner()` function
|
||||
|
||||
### Database Schema (SQLite)
|
||||
- `users` - User accounts and authentication
|
||||
- `configs` - GitHub/Gitea connection settings
|
||||
@@ -91,7 +98,7 @@ export async function POST({ request }: APIContext) {
|
||||
- Uses Bun's native test runner (`bun:test`)
|
||||
- Test files use `.test.ts` or `.test.tsx` extension
|
||||
- Setup file at `/src/tests/setup.bun.ts`
|
||||
- Mock utilities available for API testing
|
||||
- Mock utilities available for API testing.
|
||||
|
||||
### Development Tips
|
||||
- Environment variables in `.env` (copy from `.env.example`)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
FROM oven/bun:1.2.16-alpine AS base
|
||||
FROM oven/bun:1.2.18-alpine AS base
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl
|
||||
|
||||
|
||||
632
LICENSE
632
LICENSE
@@ -1,21 +1,619 @@
|
||||
MIT License
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2025 ARUNAVO RAY
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
123
README.md
123
README.md
@@ -3,13 +3,23 @@
|
||||
<h1>Gitea Mirror</h1>
|
||||
<p><i>A modern web app for automatically mirroring repositories from GitHub to your self-hosted Gitea.</i></p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/releases/latest"><img src="https://img.shields.io/github/v/tag/arunavo4/gitea-mirror?label=release" alt="release"/></a>
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/actions/workflows/astro-build-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/arunavo4/gitea-mirror/astro-build-test.yml?branch=main" alt="build"/></a>
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/pkgs/container/gitea-mirror"><img src="https://img.shields.io/badge/ghcr.io-container-blue?logo=github" alt="container"/></a>
|
||||
<a href="https://github.com/arunavo4/gitea-mirror/blob/main/LICENSE"><img src="https://img.shields.io/github/license/arunavo4/gitea-mirror" alt="license"/></a>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/releases/latest"><img src="https://img.shields.io/github/v/tag/RayLabsHQ/gitea-mirror?label=release" alt="release"/></a>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/actions/workflows/astro-build-test.yml"><img src="https://img.shields.io/github/actions/workflow/status/RayLabsHQ/gitea-mirror/astro-build-test.yml?branch=main" alt="build"/></a>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/pkgs/container/gitea-mirror"><img src="https://img.shields.io/badge/ghcr.io-container-blue?logo=github" alt="container"/></a>
|
||||
<a href="https://github.com/RayLabsHQ/gitea-mirror/blob/main/LICENSE"><img src="https://img.shields.io/github/license/RayLabsHQ/gitea-mirror" alt="license"/></a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Repository Migration Notice**: Starting from version 2.20, this project has moved from `arunavo4/gitea-mirror` to `RayLabsHQ/gitea-mirror`. Please update your Docker images and Git remotes to use the new location:
|
||||
> ```bash
|
||||
> # Docker
|
||||
> docker pull ghcr.io/raylabshq/gitea-mirror:latest
|
||||
>
|
||||
> # Git remote
|
||||
> git remote set-url origin https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
> ```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
@@ -36,9 +46,10 @@ See the [LXC Container Deployment Guide](scripts/README-lxc.md).
|
||||
## ✨ Features
|
||||
|
||||
- 🔁 Sync public, private, or starred GitHub repos to Gitea
|
||||
- 🏢 Mirror entire organizations with structure preservation
|
||||
- 🏢 Mirror entire organizations with flexible organization strategies
|
||||
- 🎯 Custom destination control for both organizations and individual repositories
|
||||
- 🐞 Optional mirroring of issues and labels
|
||||
- 🌟 Mirror your starred repositories
|
||||
- 🌟 Mirror your starred repositories to a dedicated organization
|
||||
- 🕹️ Modern user interface with toast notifications and smooth experience
|
||||
- 🧠 Smart filtering and job queue with detailed logs
|
||||
- 🛠️ Works with personal access tokens (GitHub + Gitea)
|
||||
@@ -136,12 +147,12 @@ If you want to run the container directly without Docker Compose:
|
||||
|
||||
```bash
|
||||
# Pull the latest multi-architecture image
|
||||
docker pull ghcr.io/arunavo4/gitea-mirror:latest
|
||||
docker pull ghcr.io/RayLabsHQ/gitea-mirror:latest
|
||||
|
||||
# Run the application with a volume for persistent data
|
||||
docker run -d -p 4321:4321 \
|
||||
-v gitea-mirror-data:/app/data \
|
||||
ghcr.io/arunavo4/gitea-mirror:latest
|
||||
ghcr.io/RayLabsHQ/gitea-mirror:latest
|
||||
```
|
||||
|
||||
##### Building Docker Images Manually
|
||||
@@ -167,36 +178,27 @@ docker compose up -d
|
||||
|
||||
See [Docker build documentation](./scripts/README-docker.md) for more details.
|
||||
|
||||
##### Using LXC Containers
|
||||
##### Using LXC Containers (Proxmox VE)
|
||||
|
||||
Gitea Mirror offers two deployment options for LXC containers:
|
||||
|
||||
**1. Proxmox VE (online, recommended for production)**
|
||||
For Proxmox VE users, Gitea Mirror can be deployed using the community-maintained script:
|
||||
|
||||
```bash
|
||||
# One-command installation on Proxmox VE
|
||||
# Uses the community-maintained script by Tobias ([CrazyWolf13](https://github.com/CrazyWolf13))
|
||||
# at [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE)
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"
|
||||
```
|
||||
|
||||
**2. Local testing (offline-friendly, works on developer laptops)**
|
||||
This community script:
|
||||
- Creates a privileged Alpine Linux LXC container
|
||||
- Installs Bun runtime environment
|
||||
- Clones and builds Gitea Mirror
|
||||
- Configures a systemd service for automatic startup
|
||||
- Sets up the application to run on port 4321
|
||||
|
||||
```bash
|
||||
# Download the script
|
||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
||||
chmod +x gitea-mirror-lxc-local.sh
|
||||
> [!NOTE]
|
||||
> The script is maintained by the [Community Scripts for Proxmox VE](https://community-scripts.github.io/ProxmoxVE/) project.
|
||||
> For more information, visit the [Gitea Mirror script documentation](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror).
|
||||
|
||||
# Run with your local repo directory
|
||||
sudo LOCAL_REPO_DIR=~/Development/gitea-mirror ./gitea-mirror-lxc-local.sh
|
||||
```
|
||||
|
||||
Both scripts:
|
||||
- Set up a privileged Ubuntu 22.04 LXC container
|
||||
- Install Bun runtime environment
|
||||
- Build the application
|
||||
- Configure a systemd service
|
||||
- Start the service automatically
|
||||
After installation, access Gitea Mirror at `http://<container-ip>:4321`
|
||||
|
||||
The application includes a health check endpoint at `/api/health` for monitoring.
|
||||
|
||||
@@ -242,7 +244,7 @@ The Docker container can be configured with the following environment variables:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
|
||||
# Quick setup (installs dependencies and initializes the database)
|
||||
@@ -326,6 +328,57 @@ Key configuration options include:
|
||||
> [!IMPORTANT]
|
||||
> **SQLite is the only database required for Gitea Mirror**, handling both data storage and real-time event notifications.
|
||||
|
||||
### Mirror Strategies & Destination Customization
|
||||
|
||||
Gitea Mirror offers three flexible strategies for organizing your repositories in Gitea, with fine-grained control over destinations:
|
||||
|
||||
#### 1. **Preserve GitHub Structure** (Default)
|
||||
- Personal repositories → Your Gitea username (or custom organization)
|
||||
- Organization repositories → Same organization name in Gitea (with individual overrides)
|
||||
- Maintains the exact structure from GitHub with optional customization
|
||||
|
||||
#### 2. **Single Organization**
|
||||
- All repositories → One designated organization
|
||||
- Simplifies management by consolidating everything
|
||||
- Requires specifying a destination organization name
|
||||
|
||||
#### 3. **Flat User Structure**
|
||||
- All repositories → Your Gitea user account
|
||||
- No organizations needed
|
||||
- Simplest approach for personal use
|
||||
|
||||
#### Destination Customization
|
||||
|
||||
**Organization-Level Overrides:**
|
||||
- Click the edit button on any organization card to set a custom destination
|
||||
- All repositories from that GitHub organization will be mirrored to your specified Gitea organization
|
||||
- Visual indicators show when custom destinations are active
|
||||
|
||||
**Repository-Level Overrides:**
|
||||
- Fine-tune individual repository destinations in the repository table
|
||||
- Click the edit button in the "Destination" column to customize where a specific repo is mirrored
|
||||
- Overrides organization-level settings for maximum flexibility
|
||||
- Starred repositories display a ⭐ icon and always go to the configured starred repos organization
|
||||
|
||||
**Priority Hierarchy:**
|
||||
1. Starred repositories → Always go to `starredReposOrg` (not editable)
|
||||
2. Repository-level custom destination (highest priority for non-starred)
|
||||
3. Organization-level custom destination
|
||||
4. Personal repos override (for non-organization repos)
|
||||
5. Default strategy rules (lowest priority)
|
||||
|
||||
> [!NOTE]
|
||||
> **Starred Repositories**: Repositories you've starred on GitHub are automatically organized into a separate organization (default: "starred") and cannot have custom destinations. They're marked with a ⭐ icon for easy identification.
|
||||
|
||||
> [!TIP]
|
||||
> **Example Use Cases**:
|
||||
> - Mirror personal repos to `personal-archive` organization
|
||||
> - Redirect `work-org` repos to `company-mirror` in Gitea
|
||||
> - Override a single important repo to go to a special organization
|
||||
> - Keep `company-org` repos in their own `company-org` organization
|
||||
> - Override `community-scripts` to go to `community-mirrors` organization
|
||||
> - This gives you complete control while maintaining GitHub's structure as the default
|
||||
|
||||
## 🚀 Development
|
||||
|
||||
### Local Development Setup
|
||||
@@ -363,13 +416,13 @@ docker run -d \
|
||||
-e USER_UID=1000 \
|
||||
-e USER_GID=1000 \
|
||||
-e GITEA__database__DB_TYPE=sqlite3 \
|
||||
-e GITEA__database__PATH=/data/gitea.db \
|
||||
-e GITEA__database__PATH=/data/gitea/gitea.db \
|
||||
-e GITEA__server__DOMAIN=localhost \
|
||||
-e GITEA__server__ROOT_URL=http://localhost:3001/ \
|
||||
-e GITEA__server__SSH_DOMAIN=localhost \
|
||||
-e GITEA__server__SSH_PORT=2222 \
|
||||
-e GITEA__server__START_SSH_SERVER=true \
|
||||
-e GITEA__security__INSTALL_LOCK=true \
|
||||
-e GITEA__security__INSTALL_LOCK=false \
|
||||
-e GITEA__service__DISABLE_REGISTRATION=false \
|
||||
gitea/gitea:latest
|
||||
```
|
||||
@@ -396,7 +449,7 @@ docker run -d \
|
||||
-e GITEA_URL=http://gitea:3000 \
|
||||
-e GITEA_TOKEN=your-local-gitea-token \
|
||||
-e GITEA_USERNAME=your-local-gitea-username \
|
||||
arunavo4/gitea-mirror:latest
|
||||
RayLabsHQ/gitea-mirror:latest
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
@@ -497,14 +550,14 @@ Try the following steps:
|
||||
> # Run with a volume for persistent data storage
|
||||
> docker run -d -p 4321:4321 \
|
||||
> -v gitea-mirror-data:/app/data \
|
||||
> ghcr.io/arunavo4/gitea-mirror:latest
|
||||
> ghcr.io/RayLabsHQ/gitea-mirror:latest
|
||||
> ```
|
||||
>
|
||||
> For homelab/self-hosted setups, you can use the standard Docker Compose file which includes automatic database cleanup:
|
||||
>
|
||||
> ```bash
|
||||
> # Clone the repository
|
||||
> git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
> git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
> cd gitea-mirror
|
||||
>
|
||||
> # Start the application with Docker Compose
|
||||
|
||||
202
bun.lock
202
bun.lock
@@ -4,15 +4,18 @@
|
||||
"": {
|
||||
"name": "gitea-mirror",
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/node": "^9.2.2",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
@@ -46,6 +49,7 @@
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.25.64",
|
||||
},
|
||||
@@ -69,10 +73,14 @@
|
||||
|
||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
|
||||
|
||||
"@astrojs/check": ["@astrojs/check@0.9.4", "", { "dependencies": { "@astrojs/language-server": "^2.15.0", "chokidar": "^4.0.1", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0" }, "bin": { "astro-check": "dist/bin.js" } }, "sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA=="],
|
||||
|
||||
"@astrojs/compiler": ["@astrojs/compiler@2.12.2", "", {}, "sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw=="],
|
||||
|
||||
"@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.6.1", "", {}, "sha512-l5Pqf6uZu31aG+3Lv8nl/3s4DbUzdlxTWDof4pEpto6GUJNhhCbelVi9dEyurOVyqaelwmS9oSyOWOENSfgo9A=="],
|
||||
|
||||
"@astrojs/language-server": ["@astrojs/language-server@2.15.4", "", { "dependencies": { "@astrojs/compiler": "^2.10.3", "@astrojs/yaml2ts": "^0.2.2", "@jridgewell/sourcemap-codec": "^1.4.15", "@volar/kit": "~2.4.7", "@volar/language-core": "~2.4.7", "@volar/language-server": "~2.4.7", "@volar/language-service": "~2.4.7", "fast-glob": "^3.2.12", "muggle-string": "^0.4.1", "volar-service-css": "0.0.62", "volar-service-emmet": "0.0.62", "volar-service-html": "0.0.62", "volar-service-prettier": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", "volar-service-yaml": "0.0.62", "vscode-html-languageservice": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "bin/nodeServer.js" } }, "sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A=="],
|
||||
|
||||
"@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
||||
|
||||
"@astrojs/mdx": ["@astrojs/mdx@4.3.0", "", { "dependencies": { "@astrojs/markdown-remark": "6.3.2", "@mdx-js/mdx": "^3.1.0", "acorn": "^8.14.1", "es-module-lexer": "^1.6.0", "estree-util-visit": "^2.0.0", "hast-util-to-html": "^9.0.5", "kleur": "^4.1.5", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-smartypants": "^3.0.2", "source-map": "^0.7.4", "unist-util-visit": "^5.0.0", "vfile": "^6.0.3" }, "peerDependencies": { "astro": "^5.0.0" } }, "sha512-OGX2KvPeBzjSSKhkCqrUoDMyzFcjKt5nTE5SFw3RdoLf0nrhyCXBQcCyclzWy1+P+XpOamn+p+hm1EhpCRyPxw=="],
|
||||
@@ -85,6 +93,8 @@
|
||||
|
||||
"@astrojs/telemetry": ["@astrojs/telemetry@3.3.0", "", { "dependencies": { "ci-info": "^4.2.0", "debug": "^4.4.0", "dlv": "^1.1.3", "dset": "^3.1.4", "is-docker": "^3.0.0", "is-wsl": "^3.1.0", "which-pm-runs": "^1.1.0" } }, "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ=="],
|
||||
|
||||
"@astrojs/yaml2ts": ["@astrojs/yaml2ts@0.2.2", "", { "dependencies": { "yaml": "^2.5.0" } }, "sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
||||
@@ -135,6 +145,20 @@
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||
|
||||
"@emmetio/abbreviation": ["@emmetio/abbreviation@2.3.3", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA=="],
|
||||
|
||||
"@emmetio/css-abbreviation": ["@emmetio/css-abbreviation@2.1.8", "", { "dependencies": { "@emmetio/scanner": "^1.0.4" } }, "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw=="],
|
||||
|
||||
"@emmetio/css-parser": ["@emmetio/css-parser@0.4.0", "", { "dependencies": { "@emmetio/stream-reader": "^2.2.0", "@emmetio/stream-reader-utils": "^0.1.0" } }, "sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw=="],
|
||||
|
||||
"@emmetio/html-matcher": ["@emmetio/html-matcher@1.3.0", "", { "dependencies": { "@emmetio/scanner": "^1.0.0" } }, "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ=="],
|
||||
|
||||
"@emmetio/scanner": ["@emmetio/scanner@1.0.4", "", {}, "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA=="],
|
||||
|
||||
"@emmetio/stream-reader": ["@emmetio/stream-reader@2.2.0", "", {}, "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw=="],
|
||||
|
||||
"@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||
@@ -247,6 +271,12 @@
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
|
||||
|
||||
"@octokit/core": ["@octokit/core@7.0.2", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", "@octokit/request": "^10.0.2", "@octokit/request-error": "^7.0.0", "@octokit/types": "^14.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g=="],
|
||||
@@ -277,6 +307,8 @@
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||
|
||||
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collapsible": "1.1.11", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||
|
||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
||||
@@ -303,6 +335,8 @@
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||
|
||||
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.10", "@radix-ui/react-popper": "1.2.7", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||
@@ -525,12 +559,30 @@
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@3.2.3", "", { "dependencies": { "@vitest/pretty-format": "3.2.3", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q=="],
|
||||
|
||||
"@volar/kit": ["@volar/kit@2.4.14", "", { "dependencies": { "@volar/language-service": "2.4.14", "@volar/typescript": "2.4.14", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-kBcmHjEodtmYGJELHePZd2JdeYm4ZGOd9F/pQ1YETYIzAwy4Z491EkJ1nRSo/GTxwKt0XYwYA/dHSEgXecVHRA=="],
|
||||
|
||||
"@volar/language-core": ["@volar/language-core@2.4.14", "", { "dependencies": { "@volar/source-map": "2.4.14" } }, "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w=="],
|
||||
|
||||
"@volar/language-server": ["@volar/language-server@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "@volar/language-service": "2.4.14", "@volar/typescript": "2.4.14", "path-browserify": "^1.0.1", "request-light": "^0.7.0", "vscode-languageserver": "^9.0.1", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-P3mGbQbW0v40UYBnb3DAaNtRYx6/MGOVKzdOWmBCGwjUkCR2xBkGrCFt05XnPDwFS/cTWDh2U6Mc9lpZ8Aecfw=="],
|
||||
|
||||
"@volar/language-service": ["@volar/language-service@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "vscode-languageserver-protocol": "^3.17.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" } }, "sha512-vNC3823EJohdzLTyjZoCMPwoWCfINB5emusniCkW5CGoGHQov4VVmT6yI5ncgP/NpgAIUv2NEkJooXvLHA4VeQ=="],
|
||||
|
||||
"@volar/source-map": ["@volar/source-map@2.4.14", "", {}, "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ=="],
|
||||
|
||||
"@volar/typescript": ["@volar/typescript@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw=="],
|
||||
|
||||
"@vscode/emmet-helper": ["@vscode/emmet-helper@2.11.0", "", { "dependencies": { "emmet": "^2.4.3", "jsonc-parser": "^2.3.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.15.1", "vscode-uri": "^3.0.8" } }, "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw=="],
|
||||
|
||||
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.3", "", {}, "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@@ -569,6 +621,8 @@
|
||||
|
||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.24.5", "", { "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw=="],
|
||||
@@ -609,6 +663,8 @@
|
||||
|
||||
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
|
||||
|
||||
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||
|
||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
@@ -695,7 +751,9 @@
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.159", "", {}, "sha512-CEvHptWAMV5p6GJ0Lq8aheyvVbfzVrv5mmidu1D3pidoVNkB3tTBsTMVtPJ+rzRK5oV229mCLz9Zj/hNvU8GBA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
"emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
@@ -743,8 +801,16 @@
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"flattie": ["flattie@1.1.1", "", {}, "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ=="],
|
||||
|
||||
"fontace": ["fontace@0.3.0", "", { "dependencies": { "@types/fontkit": "^2.0.8", "fontkit": "^2.0.4" } }, "sha512-czoqATrcnxgWb/nAkfyIrRp6Q8biYj7nGnL6zfhTcX+JKKpWHFBnb8uNMw/kZr7u++3Y3wYSYoZgHkCcsuBpBg=="],
|
||||
@@ -759,6 +825,8 @@
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
@@ -767,6 +835,8 @@
|
||||
|
||||
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
@@ -835,12 +905,18 @@
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||
@@ -857,8 +933,12 @@
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
@@ -959,6 +1039,8 @@
|
||||
|
||||
"mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
@@ -1029,6 +1111,8 @@
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
@@ -1045,6 +1129,8 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||
@@ -1091,6 +1177,8 @@
|
||||
|
||||
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="],
|
||||
@@ -1101,6 +1189,8 @@
|
||||
|
||||
"postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="],
|
||||
|
||||
"prettier": ["prettier@2.8.7", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw=="],
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
"prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
|
||||
@@ -1111,6 +1201,8 @@
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
@@ -1171,6 +1263,12 @@
|
||||
|
||||
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
|
||||
|
||||
"request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="],
|
||||
@@ -1183,10 +1281,14 @@
|
||||
|
||||
"retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
||||
|
||||
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
@@ -1229,11 +1331,11 @@
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||
|
||||
@@ -1273,6 +1375,8 @@
|
||||
|
||||
"tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
|
||||
@@ -1293,8 +1397,12 @@
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="],
|
||||
|
||||
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
|
||||
|
||||
"ultrahtml": ["ultrahtml@1.6.0", "", {}, "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw=="],
|
||||
@@ -1359,6 +1467,40 @@
|
||||
|
||||
"vitest": ["vitest@3.2.3", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.3", "@vitest/mocker": "3.2.3", "@vitest/pretty-format": "^3.2.3", "@vitest/runner": "3.2.3", "@vitest/snapshot": "3.2.3", "@vitest/spy": "3.2.3", "@vitest/utils": "3.2.3", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.0", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.3", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.3", "@vitest/ui": "3.2.3", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww=="],
|
||||
|
||||
"volar-service-css": ["volar-service-css@0.0.62", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg=="],
|
||||
|
||||
"volar-service-emmet": ["volar-service-emmet@0.0.62", "", { "dependencies": { "@emmetio/css-parser": "^0.4.0", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ=="],
|
||||
|
||||
"volar-service-html": ["volar-service-html@0.0.62", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ=="],
|
||||
|
||||
"volar-service-prettier": ["volar-service-prettier@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w=="],
|
||||
|
||||
"volar-service-typescript": ["volar-service-typescript@0.0.62", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.3", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g=="],
|
||||
|
||||
"volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng=="],
|
||||
|
||||
"volar-service-yaml": ["volar-service-yaml@0.0.62", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.15.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig=="],
|
||||
|
||||
"vscode-css-languageservice": ["vscode-css-languageservice@6.3.6", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-fU4h8mT3KlvfRcbF74v/M+Gzbligav6QMx4AD/7CbclWPYOpGb9kgIswfpZVJbIcOEJJACI9iYizkNwdiAqlHw=="],
|
||||
|
||||
"vscode-html-languageservice": ["vscode-html-languageservice@5.5.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-No6Er2P2L8IsXDnUFlp0bP4f2sdkJv+zJLZYFhtEQIp+2xNfxY8WYkhSxLJ/7bZhuV/aU55lmGSSHBVxSGer3Q=="],
|
||||
|
||||
"vscode-json-languageservice": ["vscode-json-languageservice@4.1.8", "", { "dependencies": { "jsonc-parser": "^3.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2" } }, "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg=="],
|
||||
|
||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
|
||||
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||
|
||||
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
||||
|
||||
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||
|
||||
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
||||
|
||||
"vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="],
|
||||
|
||||
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
@@ -1387,8 +1529,16 @@
|
||||
|
||||
"xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
|
||||
|
||||
"yaml-language-server": ["yaml-language-server@1.15.0", "", { "dependencies": { "ajv": "^8.11.0", "lodash": "4.17.21", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^7.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-nls": "^5.0.0", "vscode-uri": "^3.0.2", "yaml": "2.2.2" }, "optionalDependencies": { "prettier": "2.8.7" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
|
||||
@@ -1447,16 +1597,20 @@
|
||||
|
||||
"@types/babel__template/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"boxen/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"boxen/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="],
|
||||
|
||||
"magicast/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
@@ -1465,12 +1619,24 @@
|
||||
|
||||
"prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="],
|
||||
|
||||
"yaml-language-server/vscode-languageserver": ["vscode-languageserver@7.0.0", "", { "dependencies": { "vscode-languageserver-protocol": "3.16.0" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw=="],
|
||||
|
||||
"yaml-language-server/yaml": ["yaml@2.2.2", "", {}, "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA=="],
|
||||
|
||||
"@astrojs/react/@vitejs/plugin-react/@babel/core": ["@babel/core@7.27.3", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/traverse": "^7.27.3", "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-hyrN8ivxfvJ4i0fIJuV4EOlV0WDMz5Ui4StRTgVaAvWeiRCilXgwVvxJKtFQ3TKtHgJscB2YiXKGNJuVwhQMtA=="],
|
||||
|
||||
"@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9", "", {}, "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w=="],
|
||||
@@ -1481,14 +1647,24 @@
|
||||
|
||||
"@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
"boxen/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
"boxen/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"widest-line/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"widest-line/string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.16.0", "", { "dependencies": { "vscode-jsonrpc": "6.0.0", "vscode-languageserver-types": "3.16.0" } }, "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A=="],
|
||||
|
||||
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/helpers": ["@babel/helpers@7.27.3", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.3" } }, "sha512-h/eKy9agOya1IGuLaZ9tEUgz+uIRXcbtOhRtUyyMf8JFmn1iT13vnl/IGVWSkdOCG/pC57U4S1jnAabAavTMwg=="],
|
||||
|
||||
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/parser": ["@babel/parser@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" }, "bin": "./bin/babel-parser.js" }, "sha512-xyYxRj6+tLNDTWi0KCBcZ9V7yg3/lwL9DWh9Uwh/RIVlIfFidggcgxKX3GCXwCiswwcGRawBKbEg2LG/Y8eJhw=="],
|
||||
@@ -1496,5 +1672,13 @@
|
||||
"@astrojs/react/@vitejs/plugin-react/@babel/core/@babel/traverse": ["@babel/traverse@7.27.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", "@babel/parser": "^7.27.3", "@babel/template": "^7.27.2", "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-lId/IfN/Ye1CIu8xG7oKBHXd2iNb2aW1ilPszzGcJug6M8RCKfVNcYhpI5+bMvFYjK7lXIM0R+a+6r8xhHp2FQ=="],
|
||||
|
||||
"@astrojs/react/@vitejs/plugin-react/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"widest-line/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@6.0.0", "", {}, "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg=="],
|
||||
|
||||
"yaml-language-server/vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.16.0", "", {}, "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ services:
|
||||
image: gitea/gitea:latest
|
||||
container_name: gitea
|
||||
restart: unless-stopped
|
||||
entrypoint: ["/tmp/gitea-dev-init.sh"]
|
||||
environment:
|
||||
- USER_UID=1000
|
||||
- USER_GID=1000
|
||||
- GITEA__database__DB_TYPE=sqlite3
|
||||
- GITEA__database__PATH=/data/gitea.db
|
||||
- GITEA__database__PATH=/data/gitea/gitea.db
|
||||
- GITEA__server__DOMAIN=localhost
|
||||
- GITEA__server__ROOT_URL=http://localhost:3001/
|
||||
- GITEA__server__SSH_DOMAIN=localhost
|
||||
@@ -19,20 +20,24 @@ services:
|
||||
- GITEA__server__START_SSH_SERVER=true
|
||||
- GITEA__security__INSTALL_LOCK=true
|
||||
- GITEA__service__DISABLE_REGISTRATION=false
|
||||
- GITEA__log__MODE=console
|
||||
- GITEA__log__LEVEL=Info
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "2222:22"
|
||||
volumes:
|
||||
- gitea-data:/data
|
||||
- gitea-config:/etc/gitea
|
||||
- ./scripts/gitea-app.ini:/tmp/app.ini:ro
|
||||
- ./scripts/gitea-dev-init.sh:/tmp/gitea-dev-init.sh:ro
|
||||
networks:
|
||||
- gitea-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/healthz"]
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
|
||||
# Development service connected to local Gitea
|
||||
gitea-mirror-dev:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-arunavo4/gitea-mirror}:${DOCKER_TAG:-latest}
|
||||
image: ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-raylabshq/gitea-mirror}:${DOCKER_TAG:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@@ -11,7 +11,7 @@ services:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
cache_from:
|
||||
- ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-arunavo4/gitea-mirror}:${DOCKER_TAG:-latest}
|
||||
- ${DOCKER_REGISTRY:-ghcr.io}/${DOCKER_IMAGE:-raylabshq/gitea-mirror}:${DOCKER_TAG:-latest}
|
||||
container_name: gitea-mirror
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "2.13.1",
|
||||
"version": "2.20.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
@@ -31,15 +31,18 @@
|
||||
"astro": "bunx --bun astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/node": "^9.2.2",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
@@ -73,6 +76,7 @@
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^11.1.0",
|
||||
"zod": "^3.25.64"
|
||||
},
|
||||
|
||||
56
scripts/README-dev.md
Normal file
56
scripts/README-dev.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Development Environment Setup
|
||||
|
||||
This directory contains scripts to help set up a development environment with a pre-configured Gitea instance.
|
||||
|
||||
## Default Credentials
|
||||
|
||||
For development convenience, the Gitea instance is pre-configured with:
|
||||
|
||||
- **Admin Username**: `admin`
|
||||
- **Admin Password**: `admin123`
|
||||
- **Gitea URL**: http://localhost:3001
|
||||
|
||||
## Files
|
||||
|
||||
- `gitea-app.ini` - Pre-configured Gitea settings for development
|
||||
- `gitea-dev-init.sh` - Initialization script that copies the config on first run
|
||||
- `gitea-init.sql` - SQL script to create default admin user (not currently used)
|
||||
|
||||
## Usage
|
||||
|
||||
1. Start the development environment:
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yml down
|
||||
docker volume rm gitea-mirror_gitea-data gitea-mirror_gitea-config
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
2. Wait for Gitea to start (check logs):
|
||||
```bash
|
||||
docker logs -f gitea
|
||||
```
|
||||
|
||||
3. Access Gitea at http://localhost:3001 and login with:
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
|
||||
4. Generate an API token:
|
||||
- Go to Settings → Applications
|
||||
- Generate New Token
|
||||
- Give it a name like "gitea-mirror"
|
||||
- Select all permissions (for development)
|
||||
- Copy the token
|
||||
|
||||
5. Configure gitea-mirror with the token in your `.env` file or through the web UI.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If Gitea doesn't start properly:
|
||||
|
||||
1. Check logs: `docker logs gitea`
|
||||
2. Ensure volumes are clean: `docker volume rm gitea-mirror_gitea-data gitea-mirror_gitea-config`
|
||||
3. Restart: `docker compose -f docker-compose.dev.yml up -d`
|
||||
|
||||
## Security Note
|
||||
|
||||
⚠️ **These credentials are for development only!** Never use these settings in production.
|
||||
@@ -1,43 +1,47 @@
|
||||
# LXC Container Deployment Guide
|
||||
|
||||
## Overview
|
||||
Run **Gitea Mirror** in an isolated LXC container, either:
|
||||
Run **Gitea Mirror** in an isolated LXC container:
|
||||
|
||||
1. **Online, on a Proxmox VE host** – script pulls everything from GitHub
|
||||
2. **Offline / LAN-only, on a developer laptop** – script pushes your local checkout + Bun ZIP
|
||||
1. **Proxmox VE (Recommended)** – Using the community-maintained script
|
||||
2. **Local Development** – Using the local LXC script for testing
|
||||
|
||||
---
|
||||
|
||||
## 1. Proxmox VE (online, recommended for prod)
|
||||
## 1. Proxmox VE Installation (Recommended)
|
||||
|
||||
### Prerequisites
|
||||
* Proxmox VE node with the default `vmbr0` bridge
|
||||
* Root shell on the node
|
||||
* Ubuntu 22.04 LXC template present (`pveam update && pveam download ...`)
|
||||
* Proxmox VE host with internet access
|
||||
* Root shell access on the Proxmox node
|
||||
|
||||
### One-command install
|
||||
|
||||
```bash
|
||||
# Community-maintained script for Proxmox VE by Tobias ([CrazyWolf13](https://github.com/CrazyWolf13))
|
||||
# at [community-scripts/ProxmoxVE](https://github.com/community-scripts/ProxmoxVE)
|
||||
# Official documentation: https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror
|
||||
# Community-maintained script from the Proxmox VE Community Scripts project
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"
|
||||
```
|
||||
|
||||
What it does:
|
||||
### What the script does:
|
||||
|
||||
* Uses the community-maintained script from [Community Scripts for Proxmox VE](https://community-scripts.github.io/ProxmoxVE/)
|
||||
* Installs dependencies and Bun runtime
|
||||
* Clones & builds `arunavo4/gitea-mirror`
|
||||
* Creates a systemd service and starts it
|
||||
* Sets up a random `JWT_SECRET` for security
|
||||
* Creates a privileged Alpine Linux LXC container
|
||||
* Installs Bun runtime environment
|
||||
* Clones the Gitea Mirror repository
|
||||
* Builds the application
|
||||
* Configures a systemd service for automatic startup
|
||||
* Sets up the application to run on port 4321
|
||||
* Generates a secure `JWT_SECRET` automatically
|
||||
|
||||
Browse to:
|
||||
### Accessing Gitea Mirror:
|
||||
|
||||
```
|
||||
http://<container-ip>:4321
|
||||
```
|
||||
|
||||
### Additional Information:
|
||||
* **Script Source**: [Community Scripts for Proxmox VE](https://github.com/community-scripts/ProxmoxVE)
|
||||
* **Documentation**: [Gitea Mirror Script Documentation](https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror)
|
||||
* **Support**: [Community Scripts Discord](https://discord.gg/fiXVvSHnBU)
|
||||
|
||||
---
|
||||
|
||||
## 2. Local testing (LXD on a workstation, works offline)
|
||||
@@ -52,7 +56,7 @@ http://<container-ip>:4321
|
||||
### Offline installer script
|
||||
|
||||
```bash
|
||||
git clone https://github.com/arunavo4/gitea-mirror.git # if not already
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git # if not already
|
||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
||||
chmod +x gitea-mirror-lxc-local.sh
|
||||
|
||||
|
||||
68
scripts/gitea-app.ini
Normal file
68
scripts/gitea-app.ini
Normal file
@@ -0,0 +1,68 @@
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = /data/gitea/gitea.db
|
||||
|
||||
[repository]
|
||||
ROOT = /data/git/repositories
|
||||
|
||||
[server]
|
||||
SSH_DOMAIN = localhost
|
||||
DOMAIN = localhost
|
||||
HTTP_PORT = 3000
|
||||
ROOT_URL = http://localhost:3001/
|
||||
DISABLE_SSH = false
|
||||
SSH_PORT = 22
|
||||
LFS_START_SERVER = true
|
||||
LFS_JWT_SECRET = _oaWNP5sCH5cSECa-K_HvCXeXhg-zN5H0cU5vVQAZr4
|
||||
OFFLINE_MODE = false
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY = vLu5OuX0EweZjDNxKPQ5V9DXXXX8cJiKpJyQylKkMVTrNdFAzlUlNdYLYfiCybu
|
||||
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MjgzMTk1MDB9.Lz0cJB_DCLmJFh8FqDX0z9IUcxfY9jPftHEGvz_WeHo
|
||||
PASSWORD_HASH_ALGO = pbkdf2
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = false
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
REGISTER_EMAIL_CONFIRM = false
|
||||
ENABLE_NOTIFY_MAIL = false
|
||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||
ENABLE_CAPTCHA = false
|
||||
DEFAULT_KEEP_EMAIL_PRIVATE = false
|
||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||
DEFAULT_ENABLE_TIMETRACKING = true
|
||||
NO_REPLY_ADDRESS = noreply.localhost
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = gQXt_D8B-VJGCvFfJ9xEj5yp8mOd6fAza8TKc9rJJYw
|
||||
|
||||
[lfs]
|
||||
PATH = /data/git/lfs
|
||||
|
||||
[mailer]
|
||||
ENABLED = false
|
||||
|
||||
[openid]
|
||||
ENABLE_OPENID_SIGNIN = true
|
||||
ENABLE_OPENID_SIGNUP = true
|
||||
|
||||
[session]
|
||||
PROVIDER = file
|
||||
|
||||
[log]
|
||||
MODE = console
|
||||
LEVEL = Info
|
||||
ROOT_PATH = /data/gitea/log
|
||||
|
||||
[repository.pull-request]
|
||||
DEFAULT_MERGE_STYLE = merge
|
||||
|
||||
[repository.signing]
|
||||
DEFAULT_TRUST_MODEL = committer
|
||||
|
||||
[actions]
|
||||
ENABLED = false
|
||||
14
scripts/gitea-create-admin.sh
Executable file
14
scripts/gitea-create-admin.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
# Create admin user for Gitea development instance
|
||||
|
||||
echo "Creating admin user for Gitea..."
|
||||
docker exec -u git gitea gitea admin user create \
|
||||
--username admin \
|
||||
--password admin123 \
|
||||
--email admin@localhost \
|
||||
--admin \
|
||||
--must-change-password=false
|
||||
|
||||
echo "Admin user created!"
|
||||
echo "Username: admin"
|
||||
echo "Password: admin123"
|
||||
32
scripts/gitea-dev-init.sh
Executable file
32
scripts/gitea-dev-init.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh
|
||||
# Initialize Gitea for development with pre-configured settings
|
||||
|
||||
# Create necessary directories
|
||||
mkdir -p /data/gitea/conf
|
||||
|
||||
# Copy pre-configured app.ini if it doesn't exist
|
||||
if [ ! -f /data/gitea/conf/app.ini ]; then
|
||||
echo "Initializing Gitea with development configuration..."
|
||||
cp /tmp/app.ini /data/gitea/conf/app.ini
|
||||
chown 1000:1000 /data/gitea/conf/app.ini
|
||||
fi
|
||||
|
||||
# Start Gitea in background
|
||||
/usr/bin/entrypoint "$@" &
|
||||
GITEA_PID=$!
|
||||
|
||||
# Wait for Gitea to be ready
|
||||
echo "Waiting for Gitea to start..."
|
||||
until wget --no-verbose --tries=1 --spider http://localhost:3000/ 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Create admin user if it doesn't exist
|
||||
if [ ! -f /data/.admin_created ]; then
|
||||
echo "Creating default admin user..."
|
||||
su git -c "gitea admin user create --username admin --password admin123 --email admin@localhost --admin --must-change-password=false" && \
|
||||
touch /data/.admin_created
|
||||
fi
|
||||
|
||||
# Keep Gitea running in foreground
|
||||
wait $GITEA_PID
|
||||
81
src/components/NotFound.tsx
Normal file
81
src/components/NotFound.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<div className="h-dvh bg-muted/30 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center pb-4">
|
||||
<div className="mx-auto mb-4 h-16 w-16 rounded-full bg-muted flex items-center justify-center">
|
||||
<FileQuestion className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold">404</h1>
|
||||
<h2 className="text-xl font-semibold mt-2">Page Not Found</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
The page you're looking for doesn't exist or has been moved.
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button asChild className="w-full">
|
||||
<a href="/">
|
||||
<Home className="mr-2 h-4 w-4" />
|
||||
Go to Dashboard
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={() => window.history.back()}>
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-card px-2 text-muted-foreground">or visit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<a
|
||||
href="/repositories"
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||
>
|
||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-xs">Repositories</span>
|
||||
</a>
|
||||
<a
|
||||
href="/config"
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||
>
|
||||
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-xs">Config</span>
|
||||
</a>
|
||||
<a
|
||||
href="/docs"
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||
>
|
||||
<BookOpen className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-xs">Docs</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Error Code */}
|
||||
<div className="text-center pt-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Error Code: <code className="font-mono">404_NOT_FOUND</code>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -97,6 +97,11 @@ export function ConfigTabs() {
|
||||
return isGitHubValid && isGiteaValid;
|
||||
};
|
||||
|
||||
const isGitHubConfigValid = (): boolean => {
|
||||
const { githubConfig } = config;
|
||||
return !!(githubConfig.username.trim() && githubConfig.token.trim());
|
||||
};
|
||||
|
||||
// Removed the problematic useEffect that was causing circular dependencies
|
||||
// The lastRun and nextRun should be managed by the backend and fetched via API
|
||||
|
||||
@@ -571,10 +576,10 @@ export function ConfigTabs() {
|
||||
<div className="flex gap-x-4">
|
||||
<Button
|
||||
onClick={handleImportGitHubData}
|
||||
disabled={isSyncing || !isConfigFormValid()}
|
||||
disabled={isSyncing || !isGitHubConfigValid()}
|
||||
title={
|
||||
!isConfigFormValid()
|
||||
? 'Please fill all required GitHub and Gitea fields'
|
||||
!isGitHubConfigValid()
|
||||
? 'Please fill GitHub username and token fields'
|
||||
: isSyncing
|
||||
? 'Import in progress'
|
||||
: 'Import GitHub Data'
|
||||
@@ -598,7 +603,7 @@ export function ConfigTabs() {
|
||||
{/* Content section - Grid layout */}
|
||||
<div className="space-y-6">
|
||||
{/* GitHub & Gitea connections - Side by side */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:items-stretch">
|
||||
<GitHubConfigForm
|
||||
config={config.githubConfig}
|
||||
setConfig={update =>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { GitHubConfigForm } from './GitHubConfigForm';
|
||||
import { GiteaConfigForm } from './GiteaConfigForm';
|
||||
import { Separator } from '../ui/separator';
|
||||
import type { GitHubConfig, GiteaConfig } from '@/types/config';
|
||||
|
||||
interface ConnectionsFormProps {
|
||||
githubConfig: GitHubConfig;
|
||||
giteaConfig: GiteaConfig;
|
||||
setGithubConfig: (update: GitHubConfig | ((prev: GitHubConfig) => GitHubConfig)) => void;
|
||||
setGiteaConfig: (update: GiteaConfig | ((prev: GiteaConfig) => GiteaConfig)) => void;
|
||||
onAutoSaveGitHub?: (config: GitHubConfig) => Promise<void>;
|
||||
onAutoSaveGitea?: (config: GiteaConfig) => Promise<void>;
|
||||
isAutoSavingGitHub?: boolean;
|
||||
isAutoSavingGitea?: boolean;
|
||||
}
|
||||
|
||||
export function ConnectionsForm({
|
||||
githubConfig,
|
||||
giteaConfig,
|
||||
setGithubConfig,
|
||||
setGiteaConfig,
|
||||
onAutoSaveGitHub,
|
||||
onAutoSaveGitea,
|
||||
isAutoSavingGitHub,
|
||||
isAutoSavingGitea,
|
||||
}: ConnectionsFormProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<GitHubConfigForm
|
||||
config={githubConfig}
|
||||
setConfig={setGithubConfig}
|
||||
onAutoSave={onAutoSaveGitHub}
|
||||
isAutoSaving={isAutoSavingGitHub}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<GiteaConfigForm
|
||||
config={giteaConfig}
|
||||
setConfig={setGiteaConfig}
|
||||
onAutoSave={onAutoSaveGitea}
|
||||
isAutoSaving={isAutoSavingGitea}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { githubApi } from "@/lib/api";
|
||||
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
||||
import { Input } from "../ui/input";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import { toast } from "sonner";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "../ui/alert";
|
||||
import { Info } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { GitHubMirrorSettings } from "./GitHubMirrorSettings";
|
||||
import { Separator } from "../ui/separator";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
|
||||
interface GitHubConfigFormProps {
|
||||
config: GitHubConfig;
|
||||
@@ -87,7 +87,7 @@ export function GitHubConfigForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full self-start">
|
||||
<Card className="w-full h-full flex flex-col">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
GitHub Configuration
|
||||
@@ -102,7 +102,7 @@ export function GitHubConfigForm({
|
||||
</Button>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-y-6">
|
||||
<CardContent className="flex flex-col gap-y-6 flex-1">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="github-username"
|
||||
@@ -123,12 +123,49 @@ export function GitHubConfigForm({
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="github-token"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
>
|
||||
GitHub Token
|
||||
</label>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<label
|
||||
htmlFor="github-token"
|
||||
className="block text-sm font-medium"
|
||||
>
|
||||
GitHub Token
|
||||
</label>
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span className="inline-flex p-0.5 hover:bg-muted rounded-sm transition-colors cursor-help">
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="right" align="start" className="w-80">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">GitHub Token Requirements</h4>
|
||||
<div className="text-sm space-y-2">
|
||||
<p>
|
||||
You need to create a <span className="font-medium">Classic GitHub PAT Token</span> with the following scopes:
|
||||
</p>
|
||||
<ul className="ml-4 space-y-1 list-disc">
|
||||
<li><code className="text-xs bg-muted px-1 py-0.5 rounded">repo</code></li>
|
||||
<li><code className="text-xs bg-muted px-1 py-0.5 rounded">admin:org</code></li>
|
||||
</ul>
|
||||
<p className="text-muted-foreground">
|
||||
The organization access is required for mirroring organization repositories.
|
||||
</p>
|
||||
<p>
|
||||
Generate tokens at{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline font-medium"
|
||||
>
|
||||
github.com/settings/tokens
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
<Input
|
||||
id="github-token"
|
||||
name="token"
|
||||
@@ -136,7 +173,7 @@ export function GitHubConfigForm({
|
||||
value={config.token}
|
||||
onChange={handleChange}
|
||||
className="bg-background"
|
||||
placeholder="Your GitHub personal access token"
|
||||
placeholder="Your GitHub token (classic) with repo and admin:org scopes"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Required for private repositories, organizations, and starred
|
||||
@@ -163,45 +200,8 @@ export function GitHubConfigForm({
|
||||
if (onAdvancedOptionsAutoSave) onAdvancedOptionsAutoSave(newOptions);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex-col items-start">
|
||||
<Alert variant="note" className="w-full">
|
||||
<AlertTriangle className="h-4 w-4 text-blue-600 dark:text-blue-400 mr-2" />
|
||||
<AlertDescription className="text-sm">
|
||||
<div className="font-semibold mb-1">Note:</div>
|
||||
<div className="mb-1">
|
||||
You need to create a{" "}
|
||||
<span className="font-semibold">Classic GitHub PAT Token</span>{" "}
|
||||
with following scopes:
|
||||
</div>
|
||||
<ul className="ml-4 mb-1 list-disc">
|
||||
<li>
|
||||
<code>repo</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>admin:org</code>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mb-1">
|
||||
The organization access is required for mirroring organization
|
||||
repositories.
|
||||
</div>
|
||||
<div>
|
||||
You can generate tokens at{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline font-medium hover:text-blue-900 dark:hover:text-blue-200"
|
||||
>
|
||||
github.com/settings/tokens
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,17 +3,22 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Info,
|
||||
GitBranch,
|
||||
Star,
|
||||
Building2,
|
||||
Lock,
|
||||
Archive,
|
||||
GitPullRequest,
|
||||
@@ -22,7 +27,9 @@ import {
|
||||
MessageSquare,
|
||||
Target,
|
||||
BookOpen,
|
||||
GitFork
|
||||
GitFork,
|
||||
ChevronDown,
|
||||
Funnel
|
||||
} from "lucide-react";
|
||||
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -69,6 +76,18 @@ export function GitHubMirrorSettings({
|
||||
|
||||
// When metadata is disabled, all components should be disabled
|
||||
const isMetadataEnabled = mirrorOptions.mirrorMetadata;
|
||||
|
||||
// Calculate what content is included for starred repos
|
||||
const starredRepoContent = {
|
||||
code: true, // Always included
|
||||
releases: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorReleases,
|
||||
issues: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||
pullRequests: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||
wiki: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
};
|
||||
|
||||
const starredContentCount = Object.entries(starredRepoContent).filter(([key, value]) => key !== 'code' && value).length;
|
||||
const totalStarredOptions = 4; // releases, issues, PRs, wiki
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -100,28 +119,162 @@ export function GitHubMirrorSettings({
|
||||
Include private repositories
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Mirror your private repositories (requires appropriate token permissions)
|
||||
Mirror your private repositories
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="starred-repos"
|
||||
checked={githubConfig.mirrorStarred}
|
||||
onCheckedChange={(checked) => handleGitHubChange('mirrorStarred', !!checked)}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="starred-repos"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
Mirror starred repositories
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Include repositories you've starred on GitHub
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="starred-repos"
|
||||
checked={githubConfig.mirrorStarred}
|
||||
onCheckedChange={(checked) => handleGitHubChange('mirrorStarred', !!checked)}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="starred-repos"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
Mirror starred repositories
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Include repositories you've starred on GitHub
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Starred repos content selection - inline to prevent layout shift */}
|
||||
<div className={cn(
|
||||
"flex items-center justify-end transition-opacity duration-200",
|
||||
githubConfig.mirrorStarred ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!githubConfig.mirrorStarred}
|
||||
className="h-8 text-xs font-normal min-w-[140px] justify-between"
|
||||
>
|
||||
<span>
|
||||
{advancedOptions.skipStarredIssues ? (
|
||||
"Code only"
|
||||
) : starredContentCount === 0 ? (
|
||||
"Code only"
|
||||
) : starredContentCount === totalStarredOptions ? (
|
||||
"Full content"
|
||||
) : (
|
||||
`${starredContentCount + 1} of ${totalStarredOptions + 1} selected`
|
||||
)}
|
||||
</span>
|
||||
<ChevronDown className="ml-2 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-72">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-medium">Starred repos content</div>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Choose what content to mirror from starred repositories.
|
||||
Selecting "Lightweight mode" will only mirror code for better performance.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="starred-lightweight"
|
||||
checked={advancedOptions.skipStarredIssues}
|
||||
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="starred-lightweight"
|
||||
className="text-sm font-normal cursor-pointer flex-1"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-medium">Lightweight mode</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Only mirror code, skip all metadata
|
||||
</div>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{!advancedOptions.skipStarredIssues && (
|
||||
<>
|
||||
<Separator className="my-2" />
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">
|
||||
Content included for starred repos:
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-xs pl-2">
|
||||
<GitBranch className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Source code</span>
|
||||
<Badge variant="secondary" className="ml-auto text-[10px] px-2 h-4">Always</Badge>
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"flex items-center gap-2 text-xs pl-2",
|
||||
starredRepoContent.releases ? "" : "opacity-50"
|
||||
)}>
|
||||
<Tag className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Releases & Tags</span>
|
||||
{starredRepoContent.releases && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"flex items-center gap-2 text-xs pl-2",
|
||||
starredRepoContent.issues ? "" : "opacity-50"
|
||||
)}>
|
||||
<MessageSquare className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Issues</span>
|
||||
{starredRepoContent.issues && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"flex items-center gap-2 text-xs pl-2",
|
||||
starredRepoContent.pullRequests ? "" : "opacity-50"
|
||||
)}>
|
||||
<GitPullRequest className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Pull Requests</span>
|
||||
{starredRepoContent.pullRequests && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||
</div>
|
||||
|
||||
<div className={cn(
|
||||
"flex items-center gap-2 text-xs pl-2",
|
||||
starredRepoContent.wiki ? "" : "opacity-50"
|
||||
)}>
|
||||
<BookOpen className="h-3 w-3 text-muted-foreground" />
|
||||
<span>Wiki</span>
|
||||
{starredRepoContent.wiki && <Badge variant="outline" className="ml-auto text-[10px] px-2 h-4">Included</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground mt-2">
|
||||
To include more content, enable them in the Content & Data section below
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -172,7 +325,7 @@ export function GitHubMirrorSettings({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="mirror-metadata"
|
||||
@@ -193,92 +346,142 @@ export function GitHubMirrorSettings({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata sub-options */}
|
||||
{mirrorOptions.mirrorMetadata && (
|
||||
<div className="ml-7 space-y-2 p-3 bg-muted/30 dark:bg-muted/10 rounded-md">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="metadata-issues"
|
||||
checked={mirrorOptions.metadataComponents.issues}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('issues', !!checked)}
|
||||
disabled={!isMetadataEnabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-issues"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<MessageSquare className="h-3 w-3" />
|
||||
Issues
|
||||
</Label>
|
||||
</div>
|
||||
{/* Metadata multi-select - inline to prevent layout shift */}
|
||||
<div className={cn(
|
||||
"flex items-center justify-end transition-opacity duration-200",
|
||||
mirrorOptions.mirrorMetadata ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!mirrorOptions.mirrorMetadata}
|
||||
className="h-8 text-xs font-normal min-w-[140px] justify-between"
|
||||
>
|
||||
<span>
|
||||
{(() => {
|
||||
const selectedCount = Object.values(mirrorOptions.metadataComponents).filter(Boolean).length;
|
||||
const totalCount = Object.keys(mirrorOptions.metadataComponents).length;
|
||||
if (selectedCount === 0) return "No items selected";
|
||||
if (selectedCount === totalCount) return "All items selected";
|
||||
return `${selectedCount} of ${totalCount} selected`;
|
||||
})()}
|
||||
</span>
|
||||
<ChevronDown className="ml-2 h-3 w-3 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-64">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-medium">Metadata to mirror</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto px-2 py-1 text-xs font-normal text-primary hover:text-primary/80"
|
||||
onClick={() => {
|
||||
const allSelected = Object.values(mirrorOptions.metadataComponents).every(Boolean);
|
||||
const newValue = !allSelected;
|
||||
|
||||
// Update all metadata components at once
|
||||
onMirrorOptionsChange({
|
||||
...mirrorOptions,
|
||||
metadataComponents: {
|
||||
issues: newValue,
|
||||
pullRequests: newValue,
|
||||
labels: newValue,
|
||||
milestones: newValue,
|
||||
wiki: newValue,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Object.values(mirrorOptions.metadataComponents).every(Boolean) ? 'Deselect all' : 'Select all'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="metadata-issues-popup"
|
||||
checked={mirrorOptions.metadataComponents.issues}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('issues', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-issues-popup"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||
>
|
||||
<MessageSquare className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Issues
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="metadata-prs"
|
||||
checked={mirrorOptions.metadataComponents.pullRequests}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('pullRequests', !!checked)}
|
||||
disabled={!isMetadataEnabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-prs"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<GitPullRequest className="h-3 w-3" />
|
||||
Pull Requests
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="metadata-prs-popup"
|
||||
checked={mirrorOptions.metadataComponents.pullRequests}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('pullRequests', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-prs-popup"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||
>
|
||||
<GitPullRequest className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Pull Requests
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="metadata-labels"
|
||||
checked={mirrorOptions.metadataComponents.labels}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('labels', !!checked)}
|
||||
disabled={!isMetadataEnabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-labels"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<Tag className="h-3 w-3" />
|
||||
Labels
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="metadata-labels-popup"
|
||||
checked={mirrorOptions.metadataComponents.labels}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('labels', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-labels-popup"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||
>
|
||||
<Tag className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Labels
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="metadata-milestones"
|
||||
checked={mirrorOptions.metadataComponents.milestones}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('milestones', !!checked)}
|
||||
disabled={!isMetadataEnabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-milestones"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<Target className="h-3 w-3" />
|
||||
Milestones
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="metadata-milestones-popup"
|
||||
checked={mirrorOptions.metadataComponents.milestones}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('milestones', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-milestones-popup"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||
>
|
||||
<Target className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Milestones
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="metadata-wiki"
|
||||
checked={mirrorOptions.metadataComponents.wiki}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('wiki', !!checked)}
|
||||
disabled={!isMetadataEnabled}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-wiki"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<BookOpen className="h-3 w-3" />
|
||||
Wiki
|
||||
</Label>
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="metadata-wiki-popup"
|
||||
checked={mirrorOptions.metadataComponents.wiki}
|
||||
onCheckedChange={(checked) => handleMetadataComponentChange('wiki', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="metadata-wiki-popup"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2 flex-1"
|
||||
>
|
||||
<BookOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Wiki
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,7 +492,7 @@ export function GitHubMirrorSettings({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<Funnel className="h-4 w-4" />
|
||||
Filtering & Behavior
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
@@ -317,42 +520,6 @@ export function GitHubMirrorSettings({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{githubConfig.mirrorStarred && (
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="skip-starred-metadata"
|
||||
checked={advancedOptions.skipStarredIssues}
|
||||
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="skip-starred-metadata"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
Lightweight starred repository mirroring
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3 w-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
When enabled, starred repositories will only mirror code,
|
||||
skipping issues, PRs, and other metadata to reduce storage
|
||||
and improve performance.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only mirror code from starred repos, skip issues and metadata
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,24 +3,14 @@ import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import { giteaApi } from "@/lib/api";
|
||||
import type { GiteaConfig, GiteaOrgVisibility, MirrorStrategy } from "@/types/config";
|
||||
import type { GiteaConfig, MirrorStrategy } from "@/types/config";
|
||||
import { toast } from "sonner";
|
||||
import { Info } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { OrganizationStrategy } from "./OrganizationStrategy";
|
||||
import { OrganizationConfiguration } from "./OrganizationConfiguration";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
interface GiteaConfigFormProps {
|
||||
@@ -133,7 +123,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full self-start">
|
||||
<Card className="w-full h-full flex flex-col">
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||
<CardTitle className="text-lg font-semibold">
|
||||
Gitea Configuration
|
||||
@@ -148,7 +138,7 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
</Button>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col gap-y-6">
|
||||
<CardContent className="flex flex-col gap-y-6 flex-1">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="gitea-username"
|
||||
@@ -210,13 +200,25 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
<Separator />
|
||||
|
||||
<OrganizationStrategy
|
||||
strategy={mirrorStrategy}
|
||||
destinationOrg={config.organization}
|
||||
starredReposOrg={config.starredReposOrg}
|
||||
onStrategyChange={setMirrorStrategy}
|
||||
githubUsername={githubUsername}
|
||||
giteaUsername={config.username}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<OrganizationConfiguration
|
||||
strategy={mirrorStrategy}
|
||||
destinationOrg={config.organization}
|
||||
starredReposOrg={config.starredReposOrg}
|
||||
personalReposOrg={config.personalReposOrg}
|
||||
visibility={config.visibility}
|
||||
onDestinationOrgChange={(org) => {
|
||||
const newConfig = { ...config, organization: org };
|
||||
setConfig(newConfig);
|
||||
@@ -227,54 +229,18 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
setConfig(newConfig);
|
||||
if (onAutoSave) onAutoSave(newConfig);
|
||||
}}
|
||||
githubUsername={githubUsername}
|
||||
giteaUsername={config.username}
|
||||
onPersonalReposOrgChange={(org) => {
|
||||
const newConfig = { ...config, personalReposOrg: org };
|
||||
setConfig(newConfig);
|
||||
if (onAutoSave) onAutoSave(newConfig);
|
||||
}}
|
||||
onVisibilityChange={(visibility) => {
|
||||
const newConfig = { ...config, visibility };
|
||||
setConfig(newConfig);
|
||||
if (onAutoSave) onAutoSave(newConfig);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="visibility"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
>
|
||||
Organization Visibility
|
||||
</label>
|
||||
<Select
|
||||
name="visibility"
|
||||
value={config.visibility}
|
||||
onValueChange={(value) =>
|
||||
handleChange({
|
||||
target: { name: "visibility", value },
|
||||
} as React.ChangeEvent<HTMLInputElement>)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
||||
<SelectValue placeholder="Select visibility" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
||||
{(["public", "private", "limited"] as GiteaOrgVisibility[]).map(
|
||||
(option) => (
|
||||
<SelectItem
|
||||
key={option}
|
||||
value={option}
|
||||
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
{option.charAt(0).toUpperCase() + option.slice(1)}
|
||||
</SelectItem>
|
||||
)
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Visibility for newly created organizations
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="">
|
||||
{/* Footer content can be added here if needed */}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
201
src/components/config/OrganizationConfiguration.tsx
Normal file
201
src/components/config/OrganizationConfiguration.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import React from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Star, Globe, Lock, Shield, Info, MonitorCog } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { MirrorStrategy, GiteaOrgVisibility } from "@/types/config";
|
||||
|
||||
interface OrganizationConfigurationProps {
|
||||
strategy: MirrorStrategy;
|
||||
destinationOrg?: string;
|
||||
starredReposOrg?: string;
|
||||
personalReposOrg?: string;
|
||||
visibility: GiteaOrgVisibility;
|
||||
onDestinationOrgChange: (org: string) => void;
|
||||
onStarredReposOrgChange: (org: string) => void;
|
||||
onPersonalReposOrgChange: (org: string) => void;
|
||||
onVisibilityChange: (visibility: GiteaOrgVisibility) => void;
|
||||
}
|
||||
|
||||
const visibilityOptions = [
|
||||
{ value: "public" as GiteaOrgVisibility, label: "Public", icon: Globe, description: "Visible to everyone" },
|
||||
{ value: "private" as GiteaOrgVisibility, label: "Private", icon: Lock, description: "Visible to members only" },
|
||||
{ value: "limited" as GiteaOrgVisibility, label: "Limited", icon: Shield, description: "Visible to logged-in users" },
|
||||
];
|
||||
|
||||
export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps> = ({
|
||||
strategy,
|
||||
destinationOrg,
|
||||
starredReposOrg,
|
||||
personalReposOrg,
|
||||
visibility,
|
||||
onDestinationOrgChange,
|
||||
onStarredReposOrgChange,
|
||||
onPersonalReposOrgChange,
|
||||
onVisibilityChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||
<MonitorCog className="h-4 w-4" />
|
||||
Organization Configuration
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
{/* First row - Organization inputs with consistent layout */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Left column - always shows starred repos org */}
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="starredReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||
<Star className="h-3.5 w-3.5" />
|
||||
Starred Repos Organization
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Starred repositories will be organized separately in this organization</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="starredReposOrg"
|
||||
value={starredReposOrg || ""}
|
||||
onChange={(e) => onStarredReposOrgChange(e.target.value)}
|
||||
placeholder="starred"
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Keep starred repos organized separately
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right column - shows destination org for single-org/mixed, personal repos org for preserve, empty div for others */}
|
||||
{strategy === "single-org" || strategy === "mixed" ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="destinationOrg" className="text-sm font-normal flex items-center gap-2">
|
||||
{strategy === "mixed" ? "Personal Repos Organization" : "Destination Organization"}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{strategy === "mixed"
|
||||
? "Personal repositories will be mirrored to this organization, while organization repos preserve their structure"
|
||||
: "All repositories will be mirrored to this organization"
|
||||
}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="destinationOrg"
|
||||
value={destinationOrg || ""}
|
||||
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||
placeholder="github-mirrors"
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{strategy === "mixed"
|
||||
? "All personal repos will go to this organization"
|
||||
: "Organization for consolidated repositories"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : strategy === "preserve" ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="personalReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||
Personal Repos Organization
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Override where your personal repositories are mirrored (leave empty to use your username)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="personalReposOrg"
|
||||
value={personalReposOrg || ""}
|
||||
onChange={(e) => onPersonalReposOrgChange(e.target.value)}
|
||||
placeholder="my-personal-mirrors"
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Override destination for your personal repos
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:block" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Second row - Organization Visibility (always shown) */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-normal flex items-center gap-2">
|
||||
Organization Visibility
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Default visibility for newly created organizations</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{visibilityOptions.map((option) => {
|
||||
const Icon = option.icon;
|
||||
const isSelected = visibility === option.value;
|
||||
return (
|
||||
<TooltipProvider key={option.value}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onVisibilityChange(option.value)}
|
||||
className={cn(
|
||||
"flex items-center justify-between px-3 py-2 rounded-md text-sm transition-all",
|
||||
"border group",
|
||||
isSelected
|
||||
? "bg-accent border-accent-foreground/20"
|
||||
: "bg-background hover:bg-accent/50 border-input"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
<Info className="h-3 w-3 text-muted-foreground opacity-50 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="text-xs">{option.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,250 +1,274 @@
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Info, GitBranch, FolderTree, Package, Star, Building2, User, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Info, GitBranch, FolderTree, Star, Building2, User, Building } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
|
||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
|
||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||
|
||||
interface OrganizationStrategyProps {
|
||||
strategy: MirrorStrategy;
|
||||
destinationOrg?: string;
|
||||
starredReposOrg?: string;
|
||||
onStrategyChange: (strategy: MirrorStrategy) => void;
|
||||
onDestinationOrgChange: (org: string) => void;
|
||||
onStarredReposOrgChange: (org: string) => void;
|
||||
githubUsername?: string;
|
||||
giteaUsername?: string;
|
||||
}
|
||||
|
||||
const strategyConfig = {
|
||||
preserve: {
|
||||
title: "Mirror GitHub Structure",
|
||||
title: "Preserve Structure",
|
||||
icon: FolderTree,
|
||||
description: "Keep the same organization structure as GitHub",
|
||||
description: "Keep the exact same organization structure as GitHub",
|
||||
color: "text-blue-600 dark:text-blue-400",
|
||||
bgColor: "bg-blue-50 dark:bg-blue-950/20",
|
||||
borderColor: "border-blue-200 dark:border-blue-900",
|
||||
details: [
|
||||
"Personal repos → Your Gitea username",
|
||||
"Org repos → Same org name in Gitea",
|
||||
"Team structure preserved"
|
||||
]
|
||||
repoColors: {
|
||||
bg: "bg-blue-50 dark:bg-blue-950/30",
|
||||
icon: "text-blue-600 dark:text-blue-400"
|
||||
}
|
||||
},
|
||||
"single-org": {
|
||||
title: "Consolidate to One Org",
|
||||
title: "Single Organization",
|
||||
icon: Building2,
|
||||
description: "Mirror all repositories into a single organization",
|
||||
description: "Consolidate all repositories into one Gitea organization",
|
||||
color: "text-purple-600 dark:text-purple-400",
|
||||
bgColor: "bg-purple-50 dark:bg-purple-950/20",
|
||||
borderColor: "border-purple-200 dark:border-purple-900",
|
||||
details: [
|
||||
"All repos in one place",
|
||||
"Simplified management",
|
||||
"Custom organization name"
|
||||
]
|
||||
repoColors: {
|
||||
bg: "bg-purple-50 dark:bg-purple-950/30",
|
||||
icon: "text-purple-600 dark:text-purple-400"
|
||||
}
|
||||
},
|
||||
"flat-user": {
|
||||
title: "Flat User Structure",
|
||||
title: "User Repositories",
|
||||
icon: User,
|
||||
description: "Mirror all repositories under your user account",
|
||||
description: "Place all repositories directly under your user account",
|
||||
color: "text-green-600 dark:text-green-400",
|
||||
bgColor: "bg-green-50 dark:bg-green-950/20",
|
||||
borderColor: "border-green-200 dark:border-green-900",
|
||||
details: [
|
||||
"All repos under your username",
|
||||
"No organizations needed",
|
||||
"Simple and personal"
|
||||
]
|
||||
repoColors: {
|
||||
bg: "bg-green-50 dark:bg-green-950/30",
|
||||
icon: "text-green-600 dark:text-green-400"
|
||||
}
|
||||
},
|
||||
"mixed": {
|
||||
title: "Mixed Mode",
|
||||
icon: GitBranch,
|
||||
description: "user repos in single org, org repos preserve structure",
|
||||
color: "text-orange-600 dark:text-orange-400",
|
||||
bgColor: "bg-orange-50 dark:bg-orange-950/20",
|
||||
borderColor: "border-orange-200 dark:border-orange-900",
|
||||
repoColors: {
|
||||
bg: "bg-orange-50 dark:bg-orange-950/30",
|
||||
icon: "text-orange-600 dark:text-orange-400"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const StrategyVisualizer: React.FC<{
|
||||
strategy: MirrorStrategy;
|
||||
const MappingPreview: React.FC<{
|
||||
strategy: MirrorStrategy;
|
||||
config: typeof strategyConfig.preserve;
|
||||
destinationOrg?: string;
|
||||
starredReposOrg?: string;
|
||||
githubUsername?: string;
|
||||
giteaUsername?: string;
|
||||
}> = ({ strategy, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
}> = ({ strategy, config, destinationOrg, starredReposOrg, githubUsername, giteaUsername }) => {
|
||||
const displayGithubUsername = githubUsername || "<username>";
|
||||
const displayGiteaUsername = giteaUsername || "<username>";
|
||||
const isGithubPlaceholder = !githubUsername;
|
||||
const isGiteaPlaceholder = !giteaUsername;
|
||||
const renderPreserveStructure = () => (
|
||||
<div className="flex items-center justify-between gap-8 p-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<User className="h-4 w-4" />
|
||||
<span className={cn("text-sm", isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span className="text-sm">my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<Star className="h-4 w-4" />
|
||||
<span className="text-sm">awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
||||
<User className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className={cn("text-sm", isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
||||
<Building2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm">my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-blue-50 dark:bg-blue-950/30 rounded">
|
||||
<Building2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSingleOrg = () => (
|
||||
<div className="flex items-center justify-between gap-8 p-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<User className="h-4 w-4" />
|
||||
<span className={cn("text-sm", isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span className="text-sm">my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<Star className="h-4 w-4" />
|
||||
<span className="text-sm">awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 bg-purple-50 dark:bg-purple-950/30 rounded">
|
||||
<Building2 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm">{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-purple-50 dark:bg-purple-950/30 rounded">
|
||||
<Building2 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm">{destinationOrg || "github-mirrors"}/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-purple-50 dark:bg-purple-950/30 rounded">
|
||||
<Building2 className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderFlatUser = () => (
|
||||
<div className="flex items-center justify-between gap-8 p-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-3">GitHub</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<User className="h-4 w-4" />
|
||||
<span className={cn("text-sm", isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span className="text-sm">my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<Star className="h-4 w-4" />
|
||||
<span className="text-sm">awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-medium text-muted-foreground mb-3">Gitea</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
||||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className={cn("text-sm", isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
||||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className={cn("text-sm", isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-2 bg-green-50 dark:bg-green-950/30 rounded">
|
||||
<Building2 className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span className="text-sm">{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Card className="overflow-hidden">
|
||||
<CollapsibleTrigger className="w-full">
|
||||
<div className="bg-muted/50 p-3 border-b hover:bg-muted/70 transition-colors cursor-pointer">
|
||||
<h4 className="text-sm font-medium flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4" />
|
||||
Repository Mapping Preview
|
||||
</span>
|
||||
{isOpen ? (
|
||||
<ChevronUp className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</h4>
|
||||
|
||||
if (strategy === "preserve") {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<User className="h-3 w-3" />
|
||||
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{strategy === "preserve" && renderPreserveStructure()}
|
||||
{strategy === "single-org" && renderSingleOrg()}
|
||||
{strategy === "flat-user" && renderFlatUser()}
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span>my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Star className="h-3 w-3" />
|
||||
<span>awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<User className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span className={cn(isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>my-org/team-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (strategy === "single-org") {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<User className="h-3 w-3" />
|
||||
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span>my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Star className="h-3 w-3" />
|
||||
<span>awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{destinationOrg || "github-mirrors"}/team-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (strategy === "flat-user") {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<User className="h-3 w-3" />
|
||||
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span>my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Star className="h-3 w-3" />
|
||||
<span>awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<User className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span className={cn(isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<User className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span className={cn(isGiteaPlaceholder && "text-muted-foreground italic")}>{displayGiteaUsername}/team-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (strategy === "mixed") {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">GitHub</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<User className="h-3 w-3" />
|
||||
<span className={cn(isGithubPlaceholder && "text-muted-foreground italic")}>{displayGithubUsername}/my-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span>my-org/team-repo</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 p-1.5 bg-gray-50 dark:bg-gray-800 rounded text-xs">
|
||||
<Star className="h-3 w-3" />
|
||||
<span>awesome/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-medium text-muted-foreground mb-2">Gitea</div>
|
||||
<div className="space-y-1.5">
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{destinationOrg || "github-mirrors"}/my-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>my-org/team-repo</span>
|
||||
</div>
|
||||
<div className={cn("flex items-center gap-2 p-1.5 rounded text-xs", config.repoColors.bg)}>
|
||||
<Building2 className={cn("h-3 w-3", config.repoColors.icon)} />
|
||||
<span>{starredReposOrg || "starred"}/starred-repo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||
@@ -252,23 +276,87 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||
destinationOrg,
|
||||
starredReposOrg,
|
||||
onStrategyChange,
|
||||
onDestinationOrgChange,
|
||||
onStarredReposOrgChange,
|
||||
githubUsername,
|
||||
giteaUsername,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-1">Organization Strategy</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Choose how your repositories will be organized in Gitea
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium mb-3 flex items-center gap-2">
|
||||
<Building className="h-4 w-4" />
|
||||
Organization Strategy
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Choose how your repositories will be organized in Gitea
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<button
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-muted-foreground hover:text-foreground hover:bg-muted rounded-md transition-colors"
|
||||
type="button"
|
||||
>
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
<span>Override Options</span>
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="left" align="start" className="w-[380px]">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1.5">Fine-tune Your Mirror Destinations</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
After selecting a strategy, you can customize destinations for specific organizations and repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5 pt-2 border-t">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="text-xs font-medium">Organization Overrides</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
Click the edit button on any organization card to redirect all its repositories to a different Gitea organization.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-3.5 w-3.5 text-primary" />
|
||||
<span className="text-xs font-medium">Repository Overrides</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
Use the inline editor in the repository table's "Destination" column to set custom destinations for individual repositories.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||
<span className="text-xs font-medium">Starred Repositories</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground pl-5">
|
||||
Always go to the configured starred repos organization and cannot be overridden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<span className="font-medium">Priority:</span> Repository override → Organization override → Strategy default
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RadioGroup value={strategy} onValueChange={onStrategyChange}>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-1 2xl:grid-cols-2 gap-4">
|
||||
{(Object.entries(strategyConfig) as [MirrorStrategy, typeof strategyConfig.preserve][]).map(([key, config]) => {
|
||||
const isSelected = strategy === key;
|
||||
const Icon = config.icon;
|
||||
@@ -283,12 +371,11 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||
!isSelected && "border-muted"
|
||||
)}
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem
|
||||
value={key}
|
||||
id={key}
|
||||
className="mt-1"
|
||||
/>
|
||||
|
||||
<div className={cn(
|
||||
@@ -296,36 +383,43 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||
isSelected ? config.bgColor : "bg-muted dark:bg-muted/50"
|
||||
)}>
|
||||
<Icon className={cn(
|
||||
"h-5 w-5",
|
||||
"h-4 w-4",
|
||||
isSelected ? config.color : "text-muted-foreground dark:text-muted-foreground/70"
|
||||
)} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h4 className="font-medium">{config.title}</h4>
|
||||
{isSelected && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Selected
|
||||
</Badge>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="font-medium text-sm">{config.title}</h4>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{config.description}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1">
|
||||
{config.details.map((detail, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<div className={cn(
|
||||
"h-1.5 w-1.5 rounded-full",
|
||||
isSelected ? config.bgColor : "bg-muted dark:bg-muted/50"
|
||||
)} />
|
||||
<span className="text-xs text-muted-foreground">{detail}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span
|
||||
className="inline-flex p-1.5 hover:bg-muted rounded-md transition-colors cursor-help"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent side="left" align="center" className="w-[500px]">
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-sm">Repository Mapping Preview</h4>
|
||||
<MappingPreview
|
||||
strategy={key}
|
||||
config={config}
|
||||
destinationOrg={destinationOrg}
|
||||
starredReposOrg={starredReposOrg}
|
||||
githubUsername={githubUsername}
|
||||
giteaUsername={giteaUsername}
|
||||
/>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -335,76 +429,6 @@ export const OrganizationStrategy: React.FC<OrganizationStrategyProps> = ({
|
||||
})}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
{strategy === "single-org" && (
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4 border-purple-200 dark:border-purple-900 bg-purple-50/50 dark:bg-purple-950/20">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="destinationOrg" className="flex items-center gap-2">
|
||||
Destination Organization
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>All repositories will be mirrored to this organization</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="destinationOrg"
|
||||
value={destinationOrg || ""}
|
||||
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||
placeholder="github-mirrors"
|
||||
className="mt-1.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="p-4 border-orange-200 dark:border-orange-900 bg-orange-50/50 dark:bg-orange-950/20">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="starredReposOrg" className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
Starred Repositories Organization
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Starred repositories will be organized separately in this organization</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="starredReposOrg"
|
||||
value={starredReposOrg || ""}
|
||||
onChange={(e) => onStarredReposOrgChange(e.target.value)}
|
||||
placeholder="starred"
|
||||
className="mt-1.5"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground dark:text-muted-foreground/70 mt-1">
|
||||
Keep starred repos organized separately from your own repositories
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<StrategyVisualizer
|
||||
strategy={strategy}
|
||||
destinationOrg={destinationOrg}
|
||||
starredReposOrg={starredReposOrg}
|
||||
githubUsername={githubUsername}
|
||||
giteaUsername={giteaUsername}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
193
src/components/organizations/MirrorDestinationEditor.tsx
Normal file
193
src/components/organizations/MirrorDestinationEditor.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import { useState } from "react";
|
||||
import { ArrowRight, Edit3, RotateCcw, CheckCircle2, Building2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MirrorDestinationEditorProps {
|
||||
organizationId: string;
|
||||
organizationName: string;
|
||||
currentDestination?: string;
|
||||
onUpdate: (newDestination: string | null) => Promise<void>;
|
||||
isUpdating?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function MirrorDestinationEditor({
|
||||
organizationId,
|
||||
organizationName,
|
||||
currentDestination,
|
||||
onUpdate,
|
||||
isUpdating = false,
|
||||
className,
|
||||
}: MirrorDestinationEditorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [editValue, setEditValue] = useState(currentDestination || "");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const hasOverride = currentDestination && currentDestination !== organizationName;
|
||||
const effectiveDestination = currentDestination || organizationName;
|
||||
|
||||
const handleSave = async () => {
|
||||
const trimmedValue = editValue.trim();
|
||||
const newDestination = trimmedValue === "" || trimmedValue === organizationName
|
||||
? null
|
||||
: trimmedValue;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onUpdate(newDestination);
|
||||
setIsOpen(false);
|
||||
toast.success(
|
||||
newDestination
|
||||
? `Destination updated to: ${newDestination}`
|
||||
: "Destination reset to default"
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error("Failed to update destination");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
setEditValue("");
|
||||
await handleSave();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditValue(currentDestination || "");
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2", className)}>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<Building2 className="h-3 w-3" />
|
||||
<span className="font-medium">{organizationName}</span>
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
<span className={cn(
|
||||
"font-medium",
|
||||
hasOverride && "text-orange-600 dark:text-orange-400"
|
||||
)}>
|
||||
{effectiveDestination}
|
||||
</span>
|
||||
{hasOverride && (
|
||||
<Badge variant="outline" className="h-4 px-1 text-[10px] border-orange-600 text-orange-600 dark:border-orange-400 dark:text-orange-400">
|
||||
custom
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 p-0 opacity-60 hover:opacity-100"
|
||||
title="Edit mirror destination"
|
||||
disabled={isUpdating || isLoading}
|
||||
>
|
||||
<Edit3 className="h-3 w-3" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="end">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1">Mirror Destination</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Customize where this organization's repositories are mirrored to in Gitea.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Visual Preview */}
|
||||
<div className="rounded-md bg-muted/50 p-3 space-y-2">
|
||||
<div className="text-xs font-medium text-muted-foreground">Preview</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||
<span>{organizationName}</span>
|
||||
</div>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Building2 className="h-4 w-4 text-primary" />
|
||||
<span className="font-medium text-primary">
|
||||
{editValue.trim() || organizationName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Field */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="destination" className="text-xs">
|
||||
Destination Organization
|
||||
</Label>
|
||||
<Input
|
||||
id="destination"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
placeholder={organizationName}
|
||||
className="h-8"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave empty to use the default GitHub organization name
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
{hasOverride && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
disabled={isLoading}
|
||||
className="w-full h-8 text-xs"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3 mr-2" />
|
||||
Reset to Default ({organizationName})
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || (editValue.trim() === (currentDestination || ""))}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-3 w-3 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
"Save"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -404,6 +404,7 @@ export function Organization() {
|
||||
loadingOrgIds={loadingOrgIds}
|
||||
onMirror={handleMirrorOrg}
|
||||
onAddOrganization={() => setIsDialogOpen(true)}
|
||||
onRefresh={() => fetchOrganizations(false)}
|
||||
/>
|
||||
|
||||
<AddOrganizationDialog
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { useMemo } from "react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, RefreshCw, Building2 } from "lucide-react";
|
||||
import { SiGithub } from "react-icons/si";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Plus, RefreshCw, Building2, Check, AlertCircle, Clock } from "lucide-react";
|
||||
import { SiGithub, SiGitea } from "react-icons/si";
|
||||
import type { Organization } from "@/lib/db/schema";
|
||||
import type { FilterParams } from "@/types/filter";
|
||||
import Fuse from "fuse.js";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { getStatusColor } from "@/lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MirrorDestinationEditor } from "./MirrorDestinationEditor";
|
||||
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||
|
||||
interface OrganizationListProps {
|
||||
organizations: Organization[];
|
||||
@@ -18,8 +20,25 @@ interface OrganizationListProps {
|
||||
onMirror: ({ orgId }: { orgId: string }) => Promise<void>;
|
||||
loadingOrgIds: Set<string>;
|
||||
onAddOrganization?: () => void;
|
||||
onRefresh?: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Helper function to get status badge variant and icon
|
||||
const getStatusBadge = (status: string | null) => {
|
||||
switch (status) {
|
||||
case "imported":
|
||||
return { variant: "secondary" as const, label: "Not Mirrored", icon: null };
|
||||
case "mirroring":
|
||||
return { variant: "outline" as const, label: "Mirroring", icon: Clock };
|
||||
case "mirrored":
|
||||
return { variant: "default" as const, label: "Mirrored", icon: Check };
|
||||
case "failed":
|
||||
return { variant: "destructive" as const, label: "Failed", icon: AlertCircle };
|
||||
default:
|
||||
return { variant: "secondary" as const, label: "Unknown", icon: null };
|
||||
}
|
||||
};
|
||||
|
||||
export function OrganizationList({
|
||||
organizations,
|
||||
isLoading,
|
||||
@@ -28,7 +47,59 @@ export function OrganizationList({
|
||||
onMirror,
|
||||
loadingOrgIds,
|
||||
onAddOrganization,
|
||||
onRefresh,
|
||||
}: OrganizationListProps) {
|
||||
const { giteaConfig } = useGiteaConfig();
|
||||
|
||||
// Helper function to construct Gitea organization URL
|
||||
const getGiteaOrgUrl = (organization: Organization): string | null => {
|
||||
if (!giteaConfig?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only provide Gitea links for organizations that have been mirrored
|
||||
const validStatuses = ['mirroring', 'mirrored'];
|
||||
if (!validStatuses.includes(organization.status || '')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use destinationOrg if available, otherwise use the organization name
|
||||
const orgName = organization.destinationOrg || organization.name;
|
||||
if (!orgName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the base URL doesn't have a trailing slash
|
||||
const baseUrl = giteaConfig.url.endsWith('/')
|
||||
? giteaConfig.url.slice(0, -1)
|
||||
: giteaConfig.url;
|
||||
|
||||
return `${baseUrl}/${orgName}`;
|
||||
};
|
||||
|
||||
const handleUpdateDestination = async (orgId: string, newDestination: string | null) => {
|
||||
// Call API to update organization destination
|
||||
const response = await fetch(`/api/organizations/${orgId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
destinationOrg: newDestination,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to update organization");
|
||||
}
|
||||
|
||||
// Refresh organizations data
|
||||
if (onRefresh) {
|
||||
await onRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
const hasAnyFilter = Object.values(filter).some(
|
||||
(val) => val?.toString().trim() !== ""
|
||||
);
|
||||
@@ -93,29 +164,56 @@ export function OrganizationList({
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredOrganizations.map((org, index) => {
|
||||
const isLoading = loadingOrgIds.has(org.id ?? "");
|
||||
const statusBadge = getStatusBadge(org.status);
|
||||
const StatusIcon = statusBadge.icon;
|
||||
|
||||
return (
|
||||
<Card key={index} className="overflow-hidden p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||
<a
|
||||
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||
className="font-medium hover:underline cursor-pointer"
|
||||
>
|
||||
{org.name}
|
||||
</a>
|
||||
<Card
|
||||
key={index}
|
||||
className={cn(
|
||||
"overflow-hidden p-4 transition-all hover:shadow-md min-h-[160px]",
|
||||
isLoading && "opacity-75"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Building2 className="h-5 w-5 text-muted-foreground" />
|
||||
<a
|
||||
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||
className="font-medium hover:underline cursor-pointer"
|
||||
>
|
||||
{org.name}
|
||||
</a>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full capitalize ${
|
||||
org.membershipRole === "member"
|
||||
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
|
||||
}`}
|
||||
>
|
||||
{org.membershipRole}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Destination override section */}
|
||||
<div className="mt-2">
|
||||
<MirrorDestinationEditor
|
||||
organizationId={org.id!}
|
||||
organizationName={org.name!}
|
||||
currentDestination={org.destinationOrg}
|
||||
onUpdate={(newDestination) => handleUpdateDestination(org.id!, newDestination)}
|
||||
isUpdating={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded-full capitalize ${
|
||||
org.membershipRole === "member"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: "bg-purple-100 text-purple-800"
|
||||
}`}
|
||||
>
|
||||
{org.membershipRole}
|
||||
{/* needs to be updated */}
|
||||
</span>
|
||||
<Badge variant={statusBadge.variant} className="ml-2">
|
||||
{StatusIcon && <StatusIcon className={cn(
|
||||
"h-3 w-3",
|
||||
org.status === "mirroring" && "animate-pulse"
|
||||
)} />}
|
||||
{statusBadge.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground mb-4">
|
||||
@@ -125,78 +223,147 @@ export function OrganizationList({
|
||||
{org.repositoryCount === 1 ? "repository" : "repositories"}
|
||||
</span>
|
||||
</div>
|
||||
{(org.publicRepositoryCount !== undefined ||
|
||||
org.privateRepositoryCount !== undefined ||
|
||||
org.forkRepositoryCount !== undefined) && (
|
||||
<div className="flex gap-4 mt-2 text-xs">
|
||||
{org.publicRepositoryCount !== undefined && (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
{org.publicRepositoryCount} public
|
||||
</span>
|
||||
)}
|
||||
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 rounded-full bg-orange-500" />
|
||||
{org.privateRepositoryCount} private
|
||||
</span>
|
||||
)}
|
||||
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* Always render this section to prevent layout shift */}
|
||||
<div className="flex gap-4 mt-2 text-xs min-h-[20px]">
|
||||
{isLoading || (org.status === "mirroring" && org.publicRepositoryCount === undefined) ? (
|
||||
<>
|
||||
<Skeleton className="h-3 w-16" />
|
||||
<Skeleton className="h-3 w-16" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{org.publicRepositoryCount !== undefined ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 rounded-full bg-green-500" />
|
||||
{org.publicRepositoryCount} public
|
||||
</span>
|
||||
) : null}
|
||||
{org.privateRepositoryCount !== undefined && org.privateRepositoryCount > 0 ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 rounded-full bg-orange-500" />
|
||||
{org.privateRepositoryCount} private
|
||||
</span>
|
||||
) : null}
|
||||
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
||||
{org.forkRepositoryCount} fork{org.forkRepositoryCount !== 1 ? 's' : ''}
|
||||
</span>
|
||||
) : null}
|
||||
{/* Show a placeholder if no counts are available to maintain height */}
|
||||
{org.publicRepositoryCount === undefined &&
|
||||
org.privateRepositoryCount === undefined &&
|
||||
org.forkRepositoryCount === undefined && (
|
||||
<span className="invisible">Loading counts...</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id={`include-${org.id}`}
|
||||
name={`include-${org.id}`}
|
||||
checked={org.status === "mirrored"}
|
||||
disabled={
|
||||
loadingOrgIds.has(org.id ?? "") ||
|
||||
org.status === "mirrored" ||
|
||||
org.status === "mirroring"
|
||||
}
|
||||
onCheckedChange={async (checked) => {
|
||||
if (checked && !org.isIncluded && org.id) {
|
||||
onMirror({ orgId: org.id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`include-${org.id}`}
|
||||
className="ml-2 text-sm select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Enable mirroring
|
||||
</label>
|
||||
|
||||
{isLoading && (
|
||||
<RefreshCw className="opacity-50 h-4 w-4 animate-spin ml-4" />
|
||||
<div className="flex items-center gap-2">
|
||||
{org.status === "imported" && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||
Starting...
|
||||
</>
|
||||
) : (
|
||||
"Mirror"
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{org.status === "mirroring" && (
|
||||
<Button size="sm" disabled variant="outline">
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||
Mirroring...
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{org.status === "mirrored" && (
|
||||
<Button size="sm" disabled variant="secondary">
|
||||
<Check className="h-3 w-3 mr-2" />
|
||||
Mirrored
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{org.status === "failed" && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => org.id && onMirror({ orgId: org.id })}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-2" />
|
||||
Retrying...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="h-3 w-3 mr-2" />
|
||||
Retry
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<a
|
||||
href={`https://github.com/${org.name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<SiGithub className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{(() => {
|
||||
const giteaUrl = getGiteaOrgUrl(org);
|
||||
|
||||
{/* dont know if this looks good. maybe revised */}
|
||||
<div className="flex items-center gap-2 justify-end mt-4">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${getStatusColor(org.status)}`}
|
||||
/>
|
||||
<span className="text-sm capitalize">{org.status}</span>
|
||||
// Determine tooltip based on status and configuration
|
||||
let tooltip: string;
|
||||
if (!giteaConfig?.url) {
|
||||
tooltip = "Gitea not configured";
|
||||
} else if (org.status === 'imported') {
|
||||
tooltip = "Organization not yet mirrored to Gitea";
|
||||
} else if (org.status === 'failed') {
|
||||
tooltip = "Organization mirroring failed";
|
||||
} else if (org.status === 'mirroring') {
|
||||
tooltip = "Organization is being mirrored to Gitea";
|
||||
} else if (giteaUrl) {
|
||||
tooltip = "View on Gitea";
|
||||
} else {
|
||||
tooltip = "Gitea organization not available";
|
||||
}
|
||||
|
||||
return giteaUrl ? (
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<a
|
||||
href={giteaUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={tooltip}
|
||||
>
|
||||
<SiGitea className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="ghost" size="icon" disabled title={tooltip}>
|
||||
<SiGitea className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<a
|
||||
href={`https://github.com/${org.name}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="View on GitHub"
|
||||
>
|
||||
<SiGithub className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
187
src/components/repositories/InlineDestinationEditor.tsx
Normal file
187
src/components/repositories/InlineDestinationEditor.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Edit3, Check, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Repository } from "@/lib/db/schema";
|
||||
|
||||
interface InlineDestinationEditorProps {
|
||||
repository: Repository;
|
||||
giteaConfig: any;
|
||||
onUpdate: (repoId: string, newDestination: string | null) => Promise<void>;
|
||||
isUpdating?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function InlineDestinationEditor({
|
||||
repository,
|
||||
giteaConfig,
|
||||
onUpdate,
|
||||
isUpdating = false,
|
||||
className,
|
||||
}: InlineDestinationEditorProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Determine the default destination based on repository properties and config
|
||||
const getDefaultDestination = () => {
|
||||
// Starred repos always go to the configured starredReposOrg
|
||||
if (repository.isStarred && giteaConfig?.starredReposOrg) {
|
||||
return giteaConfig.starredReposOrg;
|
||||
}
|
||||
|
||||
// Check mirror strategy
|
||||
const strategy = giteaConfig?.mirrorStrategy || 'preserve';
|
||||
|
||||
if (strategy === 'single-org' && giteaConfig?.organization) {
|
||||
// All repos go to a single organization
|
||||
return giteaConfig.organization;
|
||||
} else if (strategy === 'flat-user') {
|
||||
// All repos go under the user account
|
||||
return giteaConfig?.username || repository.owner;
|
||||
} else {
|
||||
// 'preserve' strategy or default
|
||||
// For organization repos, use the organization name
|
||||
if (repository.organization) {
|
||||
return repository.organization;
|
||||
}
|
||||
// For personal repos, check if personalReposOrg is configured
|
||||
if (!repository.organization && giteaConfig?.personalReposOrg) {
|
||||
return giteaConfig.personalReposOrg;
|
||||
}
|
||||
// Default to the gitea username or owner
|
||||
return giteaConfig?.username || repository.owner;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultDestination = getDefaultDestination();
|
||||
const currentDestination = repository.destinationOrg || defaultDestination;
|
||||
const hasOverride = repository.destinationOrg && repository.destinationOrg !== defaultDestination;
|
||||
const isStarredRepo = repository.isStarred && giteaConfig?.starredReposOrg;
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditing && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.select();
|
||||
}
|
||||
}, [isEditing]);
|
||||
|
||||
const handleStartEdit = () => {
|
||||
if (isStarredRepo) return; // Don't allow editing starred repos
|
||||
setEditValue(currentDestination);
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const trimmedValue = editValue.trim();
|
||||
const newDestination = trimmedValue === defaultDestination ? null : trimmedValue;
|
||||
|
||||
if (trimmedValue === currentDestination) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onUpdate(repository.id!, newDestination);
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
// Revert on error
|
||||
setEditValue(currentDestination);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditValue(currentDestination);
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1", className)}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleCancel}
|
||||
className="h-6 text-sm px-2 py-0 w-24"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-0.5", className)}>
|
||||
{/* Show GitHub org if exists */}
|
||||
{repository.organization && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{repository.organization}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Show Gitea destination */}
|
||||
<div className="flex items-center gap-1 group">
|
||||
<span className="text-sm">
|
||||
{currentDestination || "-"}
|
||||
</span>
|
||||
{hasOverride && (
|
||||
<Badge variant="outline" className="h-4 px-1 text-[10px] ml-1">
|
||||
custom
|
||||
</Badge>
|
||||
)}
|
||||
{isStarredRepo && (
|
||||
<Badge variant="secondary" className="h-4 px-1 text-[10px] ml-1">
|
||||
starred
|
||||
</Badge>
|
||||
)}
|
||||
{!isStarredRepo && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-4 w-4 p-0 opacity-0 group-hover:opacity-60 hover:opacity-100 ml-1"
|
||||
onClick={handleStartEdit}
|
||||
disabled={isUpdating || isLoading}
|
||||
title="Edit destination"
|
||||
>
|
||||
<Edit3 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search, RefreshCw, FlipHorizontal } from "lucide-react";
|
||||
import { Search, RefreshCw, FlipHorizontal, RotateCcw, X } from "lucide-react";
|
||||
import type { MirrorRepoRequest, MirrorRepoResponse } from "@/types/mirror";
|
||||
import { useSSE } from "@/hooks/useSEE";
|
||||
import { useFilterParams } from "@/hooks/useFilterParams";
|
||||
@@ -46,6 +46,7 @@ export default function Repository() {
|
||||
owner: "",
|
||||
});
|
||||
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(false);
|
||||
const [selectedRepoIds, setSelectedRepoIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Read organization filter from URL when component mounts
|
||||
useEffect(() => {
|
||||
@@ -254,6 +255,143 @@ export default function Repository() {
|
||||
}
|
||||
};
|
||||
|
||||
// Bulk action handlers
|
||||
const handleBulkMirror = async () => {
|
||||
if (selectedRepoIds.size === 0) return;
|
||||
|
||||
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||
const eligibleRepos = selectedRepos.filter(
|
||||
repo => repo.status === "imported" || repo.status === "failed"
|
||||
);
|
||||
|
||||
if (eligibleRepos.length === 0) {
|
||||
toast.info("No eligible repositories to mirror in selection");
|
||||
return;
|
||||
}
|
||||
|
||||
const repoIds = eligibleRepos.map(repo => repo.id as string);
|
||||
|
||||
setLoadingRepoIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
repoIds.forEach(id => newSet.add(id));
|
||||
return newSet;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiRequest<MirrorRepoResponse>("/job/mirror-repo", {
|
||||
method: "POST",
|
||||
data: { userId: user?.id, repositoryIds: repoIds }
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast.success(`Mirroring started for ${repoIds.length} repositories`);
|
||||
setRepositories(prevRepos =>
|
||||
prevRepos.map(repo => {
|
||||
const updated = response.repositories.find(r => r.id === repo.id);
|
||||
return updated ? updated : repo;
|
||||
})
|
||||
);
|
||||
setSelectedRepoIds(new Set());
|
||||
} else {
|
||||
showErrorToast(response.error || "Error starting mirror jobs", toast);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
setLoadingRepoIds(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkSync = async () => {
|
||||
if (selectedRepoIds.size === 0) return;
|
||||
|
||||
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||
const eligibleRepos = selectedRepos.filter(
|
||||
repo => repo.status === "mirrored" || repo.status === "synced"
|
||||
);
|
||||
|
||||
if (eligibleRepos.length === 0) {
|
||||
toast.info("No eligible repositories to sync in selection");
|
||||
return;
|
||||
}
|
||||
|
||||
const repoIds = eligibleRepos.map(repo => repo.id as string);
|
||||
|
||||
setLoadingRepoIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
repoIds.forEach(id => newSet.add(id));
|
||||
return newSet;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiRequest<SyncRepoResponse>("/job/sync-repo", {
|
||||
method: "POST",
|
||||
data: { userId: user?.id, repositoryIds: repoIds }
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast.success(`Syncing started for ${repoIds.length} repositories`);
|
||||
setRepositories(prevRepos =>
|
||||
prevRepos.map(repo => {
|
||||
const updated = response.repositories.find(r => r.id === repo.id);
|
||||
return updated ? updated : repo;
|
||||
})
|
||||
);
|
||||
setSelectedRepoIds(new Set());
|
||||
} else {
|
||||
showErrorToast(response.error || "Error starting sync jobs", toast);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
setLoadingRepoIds(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkRetry = async () => {
|
||||
if (selectedRepoIds.size === 0) return;
|
||||
|
||||
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||
const eligibleRepos = selectedRepos.filter(repo => repo.status === "failed");
|
||||
|
||||
if (eligibleRepos.length === 0) {
|
||||
toast.info("No failed repositories in selection to retry");
|
||||
return;
|
||||
}
|
||||
|
||||
const repoIds = eligibleRepos.map(repo => repo.id as string);
|
||||
|
||||
setLoadingRepoIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
repoIds.forEach(id => newSet.add(id));
|
||||
return newSet;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await apiRequest<RetryRepoResponse>("/job/retry-repo", {
|
||||
method: "POST",
|
||||
data: { userId: user?.id, repositoryIds: repoIds }
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
toast.success(`Retrying ${repoIds.length} repositories`);
|
||||
setRepositories(prevRepos =>
|
||||
prevRepos.map(repo => {
|
||||
const updated = response.repositories.find(r => r.id === repo.id);
|
||||
return updated ? updated : repo;
|
||||
})
|
||||
);
|
||||
setSelectedRepoIds(new Set());
|
||||
} else {
|
||||
showErrorToast(response.error || "Error retrying jobs", toast);
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
setLoadingRepoIds(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncRepo = async ({ repoId }: { repoId: string }) => {
|
||||
try {
|
||||
if (!user || !user.id) {
|
||||
@@ -392,6 +530,35 @@ export default function Repository() {
|
||||
)
|
||||
).sort();
|
||||
|
||||
// Determine what actions are available for selected repositories
|
||||
const getAvailableActions = () => {
|
||||
if (selectedRepoIds.size === 0) return [];
|
||||
|
||||
const selectedRepos = repositories.filter(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||
const statuses = new Set(selectedRepos.map(repo => repo.status));
|
||||
|
||||
const actions = [];
|
||||
|
||||
// Check if any selected repos can be mirrored
|
||||
if (selectedRepos.some(repo => repo.status === "imported" || repo.status === "failed")) {
|
||||
actions.push('mirror');
|
||||
}
|
||||
|
||||
// Check if any selected repos can be synced
|
||||
if (selectedRepos.some(repo => repo.status === "mirrored" || repo.status === "synced")) {
|
||||
actions.push('sync');
|
||||
}
|
||||
|
||||
// Check if any selected repos are failed
|
||||
if (selectedRepos.some(repo => repo.status === "failed")) {
|
||||
actions.push('retry');
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
const availableActions = getAvailableActions();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8">
|
||||
{/* Combine search and actions into a single flex row */}
|
||||
@@ -459,14 +626,69 @@ export default function Repository() {
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleMirrorAllRepos}
|
||||
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
||||
>
|
||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||
Mirror All
|
||||
</Button>
|
||||
{/* Context-aware action buttons */}
|
||||
{selectedRepoIds.size === 0 ? (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleMirrorAllRepos}
|
||||
disabled={isInitialLoading || loadingRepoIds.size > 0}
|
||||
>
|
||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||
Mirror All
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-3 py-1 bg-muted/50 rounded-md">
|
||||
<span className="text-sm font-medium">
|
||||
{selectedRepoIds.size} selected
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={() => setSelectedRepoIds(new Set())}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{availableActions.includes('mirror') && (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={handleBulkMirror}
|
||||
disabled={loadingRepoIds.size > 0}
|
||||
>
|
||||
<FlipHorizontal className="h-4 w-4 mr-2" />
|
||||
Mirror ({selectedRepoIds.size})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{availableActions.includes('sync') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkSync}
|
||||
disabled={loadingRepoIds.size > 0}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
Sync ({selectedRepoIds.size})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{availableActions.includes('retry') && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkRetry}
|
||||
disabled={loadingRepoIds.size > 0}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Retry
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!isGitHubConfigured ? (
|
||||
@@ -497,6 +719,9 @@ export default function Repository() {
|
||||
onSync={handleSyncRepo}
|
||||
onRetry={handleRetryRepoAction}
|
||||
loadingRepoIds={loadingRepoIds}
|
||||
selectedRepoIds={selectedRepoIds}
|
||||
onSelectionChange={setSelectedRepoIds}
|
||||
onRefresh={() => fetchRepositories(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
import Fuse from "fuse.js";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw } from "lucide-react";
|
||||
import { FlipHorizontal, GitFork, RefreshCw, RotateCcw, Star } from "lucide-react";
|
||||
import { SiGithub, SiGitea } from "react-icons/si";
|
||||
import type { Repository } from "@/lib/db/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -9,6 +9,14 @@ import { formatDate, getStatusColor } from "@/lib/utils";
|
||||
import type { FilterParams } from "@/types/filter";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { InlineDestinationEditor } from "./InlineDestinationEditor";
|
||||
|
||||
interface RepositoryTableProps {
|
||||
repositories: Repository[];
|
||||
@@ -20,6 +28,9 @@ interface RepositoryTableProps {
|
||||
onSync: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||
onRetry: ({ repoId }: { repoId: string }) => Promise<void>;
|
||||
loadingRepoIds: Set<string>;
|
||||
selectedRepoIds: Set<string>;
|
||||
onSelectionChange: (selectedIds: Set<string>) => void;
|
||||
onRefresh?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function RepositoryTable({
|
||||
@@ -32,10 +43,36 @@ export default function RepositoryTable({
|
||||
onSync,
|
||||
onRetry,
|
||||
loadingRepoIds,
|
||||
selectedRepoIds,
|
||||
onSelectionChange,
|
||||
onRefresh,
|
||||
}: RepositoryTableProps) {
|
||||
const tableParentRef = useRef<HTMLDivElement>(null);
|
||||
const { giteaConfig } = useGiteaConfig();
|
||||
|
||||
const handleUpdateDestination = async (repoId: string, newDestination: string | null) => {
|
||||
// Call API to update repository destination
|
||||
const response = await fetch(`/api/repositories/${repoId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
destinationOrg: newDestination,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || "Failed to update repository");
|
||||
}
|
||||
|
||||
// Refresh repositories data
|
||||
if (onRefresh) {
|
||||
await onRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to construct Gitea repository URL
|
||||
const getGiteaRepoUrl = (repository: Repository): string | null => {
|
||||
if (!giteaConfig?.url) {
|
||||
@@ -105,9 +142,36 @@ export default function RepositoryTable({
|
||||
overscan: 5,
|
||||
});
|
||||
|
||||
// Selection handlers
|
||||
const handleSelectAll = (checked: boolean) => {
|
||||
if (checked) {
|
||||
const allIds = new Set(filteredRepositories.map(repo => repo.id).filter((id): id is string => !!id));
|
||||
onSelectionChange(allIds);
|
||||
} else {
|
||||
onSelectionChange(new Set());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectRepo = (repoId: string, checked: boolean) => {
|
||||
const newSelection = new Set(selectedRepoIds);
|
||||
if (checked) {
|
||||
newSelection.add(repoId);
|
||||
} else {
|
||||
newSelection.delete(repoId);
|
||||
}
|
||||
onSelectionChange(newSelection);
|
||||
};
|
||||
|
||||
const isAllSelected = filteredRepositories.length > 0 &&
|
||||
filteredRepositories.every(repo => repo.id && selectedRepoIds.has(repo.id));
|
||||
const isPartiallySelected = selectedRepoIds.size > 0 && !isAllSelected;
|
||||
|
||||
return isLoading ? (
|
||||
<div className="border rounded-md">
|
||||
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
||||
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
||||
Repository
|
||||
</div>
|
||||
@@ -132,6 +196,9 @@ export default function RepositoryTable({
|
||||
key={i}
|
||||
className="h-[65px] flex items-center justify-between border-b bg-transparent"
|
||||
>
|
||||
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||
<Skeleton className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
@@ -187,6 +254,14 @@ export default function RepositoryTable({
|
||||
<div className="flex flex-col border rounded-md">
|
||||
{/* table header */}
|
||||
<div className="h-[45px] flex items-center justify-between border-b bg-muted/50">
|
||||
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||
<Checkbox
|
||||
checked={isAllSelected}
|
||||
indeterminate={isPartiallySelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
aria-label="Select all repositories"
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full p-3 text-sm font-medium flex-[2.5]">
|
||||
Repository
|
||||
</div>
|
||||
@@ -235,11 +310,25 @@ export default function RepositoryTable({
|
||||
data-index={virtualRow.index}
|
||||
className="h-[65px] flex items-center justify-between bg-transparent border-b hover:bg-muted/50" //the height is set according to the row content. right now the highest row is in the repo column which is arround 64.99px
|
||||
>
|
||||
{/* Checkbox */}
|
||||
<div className="h-full p-3 flex items-center justify-center flex-[0.3]">
|
||||
<Checkbox
|
||||
checked={repo.id ? selectedRepoIds.has(repo.id) : false}
|
||||
onCheckedChange={(checked) => repo.id && handleSelectRepo(repo.id, !!checked)}
|
||||
aria-label={`Select ${repo.name}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Repository */}
|
||||
<div className="h-full p-3 flex items-center gap-2 flex-[2.5]">
|
||||
<GitFork className="h-4 w-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-medium">{repo.name}</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium flex items-center gap-1">
|
||||
{repo.name}
|
||||
{repo.isStarred && (
|
||||
<Star className="h-3 w-3 fill-yellow-500 text-yellow-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{repo.fullName}
|
||||
</div>
|
||||
@@ -263,7 +352,12 @@ export default function RepositoryTable({
|
||||
|
||||
{/* Organization */}
|
||||
<div className="h-full p-3 flex items-center flex-[1]">
|
||||
<p className="text-sm"> {repo.organization || "-"}</p>
|
||||
<InlineDestinationEditor
|
||||
repository={repo}
|
||||
giteaConfig={giteaConfig}
|
||||
onUpdate={handleUpdateDestination}
|
||||
isUpdating={loadingRepoIds.has(repo.id ?? "")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Last Mirrored */}
|
||||
@@ -277,12 +371,26 @@ export default function RepositoryTable({
|
||||
|
||||
{/* Status */}
|
||||
<div className="h-full p-3 flex items-center gap-x-2 flex-[1]">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${getStatusColor(
|
||||
repo.status
|
||||
)}`}
|
||||
/>
|
||||
<span className="text-sm capitalize">{repo.status}</span>
|
||||
{repo.status === "failed" && repo.errorMessage ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-x-2 cursor-help">
|
||||
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
||||
<span className="text-sm capitalize underline decoration-dotted">{repo.status}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p className="text-sm">{repo.errorMessage}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<>
|
||||
<div className={`h-2 w-2 rounded-full ${getStatusColor(repo.status)}`} />
|
||||
<span className="text-sm capitalize">{repo.status}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
42
src/components/ui/hover-card.tsx
Normal file
42
src/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function HoverCard({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
||||
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
||||
}
|
||||
|
||||
function HoverCardTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function HoverCardContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
||||
return (
|
||||
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
||||
<HoverCardPrimitive.Content
|
||||
data-slot="hover-card-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
||||
@@ -19,7 +19,11 @@ function TooltipProvider({
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
@@ -40,7 +44,7 @@ function TooltipContent({
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-[var(--radix-tooltip-content-transform-origin)] rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
|
||||
// Define a schema for the documentation collection
|
||||
const docsCollection = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
order: z.number().optional(),
|
||||
updatedDate: z.date().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Export the collections
|
||||
export const collections = {
|
||||
'docs': docsCollection,
|
||||
};
|
||||
// Export empty collections since docs have been moved
|
||||
export const collections = {};
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
---
|
||||
title: "Architecture"
|
||||
description: "Comprehensive overview of the Gitea Mirror application architecture."
|
||||
order: 1
|
||||
updatedDate: 2025-05-22
|
||||
---
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">Gitea Mirror Architecture</h1>
|
||||
<p class="text-muted-foreground mt-2">This document provides a comprehensive overview of the Gitea Mirror application architecture, including component diagrams, project structure, and detailed explanations of each part of the system.</p>
|
||||
</div>
|
||||
|
||||
## System Overview
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-muted-foreground">Gitea Mirror is a web application that automates the mirroring of GitHub repositories to Gitea instances. It provides a user-friendly interface for configuring, monitoring, and managing mirroring operations without requiring users to edit configuration files or run Docker commands.</p>
|
||||
</div>
|
||||
|
||||
The application is built using:
|
||||
|
||||
- <span class="font-semibold text-foreground">Astro</span>: Web framework for the frontend
|
||||
- <span class="font-semibold text-foreground">React</span>: Component library for interactive UI elements
|
||||
- <span class="font-semibold text-foreground">Shadcn UI</span>: UI component library built on Tailwind CSS
|
||||
- <span class="font-semibold text-foreground">SQLite</span>: Database for storing configuration, state, and events
|
||||
- <span class="font-semibold text-foreground">Bun</span>: Runtime environment for the backend
|
||||
- <span class="font-semibold text-foreground">Drizzle ORM</span>: Type-safe ORM for database interactions
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Gitea Mirror"
|
||||
Frontend["Frontend<br/>(Astro + React)"]
|
||||
Backend["Backend<br/>(Bun)"]
|
||||
Database["Database<br/>(SQLite + Drizzle)"]
|
||||
|
||||
Frontend <--> Backend
|
||||
Backend <--> Database
|
||||
end
|
||||
|
||||
subgraph "External APIs"
|
||||
GitHub["GitHub API"]
|
||||
Gitea["Gitea API"]
|
||||
end
|
||||
|
||||
Backend --> GitHub
|
||||
Backend --> Gitea
|
||||
```
|
||||
|
||||
## Component Breakdown
|
||||
|
||||
### Frontend (Astro + React)
|
||||
|
||||
The frontend is built with Astro, a modern web framework that allows for server-side rendering and partial hydration. React components are used for interactive elements, providing a responsive and dynamic user interface.
|
||||
|
||||
Key frontend components:
|
||||
|
||||
- **Dashboard**: Overview of mirroring status and recent activity
|
||||
- **Repository Management**: Interface for managing repositories to mirror
|
||||
- **Organization Management**: Interface for managing GitHub organizations
|
||||
- **Configuration**: Settings for GitHub and Gitea connections
|
||||
- **Activity Log**: Detailed log of mirroring operations
|
||||
|
||||
### Backend (Bun)
|
||||
|
||||
The backend is built with Bun and provides API endpoints for the frontend to interact with. It handles:
|
||||
|
||||
- Authentication and user management
|
||||
- GitHub API integration
|
||||
- Gitea API integration
|
||||
- Mirroring operations
|
||||
- Database interactions
|
||||
|
||||
### Database (SQLite + Drizzle ORM)
|
||||
|
||||
SQLite with Bun's native SQLite driver is used for data persistence, with Drizzle ORM providing type-safe database interactions. The database stores:
|
||||
|
||||
- User accounts and authentication data
|
||||
- GitHub and Gitea configuration
|
||||
- Repository and organization information
|
||||
- Mirroring job history and status
|
||||
- Event notifications and their read status
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **User Authentication**: Users authenticate through the frontend, which communicates with the backend to validate credentials.
|
||||
2. **Configuration**: Users configure GitHub and Gitea settings through the UI, which are stored in the SQLite database.
|
||||
3. **Repository Discovery**: The backend queries the GitHub API to discover repositories based on user configuration.
|
||||
4. **Mirroring Process**: When triggered, the backend fetches repository data from GitHub and pushes it to Gitea.
|
||||
5. **Status Tracking**: All operations are logged in the database and displayed in the Activity Log.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
gitea-mirror/
|
||||
├── src/ # Source code
|
||||
│ ├── components/ # React components
|
||||
│ ├── content/ # Documentation and content
|
||||
│ ├── layouts/ # Astro layout components
|
||||
│ ├── lib/ # Utility functions and database
|
||||
│ ├── pages/ # Astro pages and API routes
|
||||
│ └── styles/ # CSS and Tailwind styles
|
||||
├── public/ # Static assets
|
||||
├── data/ # Database and persistent data
|
||||
├── docker/ # Docker configuration
|
||||
└── scripts/ # Utility scripts for deployment and maintenance
|
||||
├── gitea-mirror-lxc-local.sh # Local LXC deployment script
|
||||
└── manage-db.ts # Database management tool
|
||||
```
|
||||
|
||||
## Deployment Options
|
||||
|
||||
Gitea Mirror supports multiple deployment options:
|
||||
|
||||
1. **Docker**: Run as a containerized application using Docker and docker-compose
|
||||
2. **LXC Containers**: Deploy in Linux Containers (LXC) on Proxmox VE (using community script by [Tobias/CrazyWolf13](https://github.com/CrazyWolf13)) or local workstations
|
||||
3. **Native**: Run directly on the host system using Bun runtime
|
||||
|
||||
Each deployment method has its own advantages:
|
||||
|
||||
- **Docker**: Isolation, easy updates, consistent environment
|
||||
- **LXC**: Lightweight virtualization, better performance than Docker, system-level isolation
|
||||
- **Native**: Best performance, direct access to system resources
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
description: "Guide to configuring Gitea Mirror for your environment."
|
||||
order: 2
|
||||
updatedDate: 2025-05-22
|
||||
---
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">Gitea Mirror Configuration Guide</h1>
|
||||
<p class="text-muted-foreground mt-2">This guide provides detailed information on how to configure Gitea Mirror for your environment.</p>
|
||||
</div>
|
||||
|
||||
## Configuration Methods
|
||||
|
||||
Gitea Mirror can be configured using:
|
||||
|
||||
1. <span class="font-semibold text-foreground">Environment Variables</span>: Set configuration options through environment variables
|
||||
2. <span class="font-semibold text-foreground">Web UI</span>: Configure the application through the web interface after installation
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables can be used to configure Gitea Mirror:
|
||||
|
||||
| Variable | Description | Default Value | Example |
|
||||
|----------|-------------|---------------|---------|
|
||||
| `NODE_ENV` | Runtime environment (development, production, test) | `development` | `production` |
|
||||
| `DATABASE_URL` | SQLite database URL | `file:data/gitea-mirror.db` | `file:path/to/your/database.db` |
|
||||
| `JWT_SECRET` | Secret key for JWT authentication | Auto-generated secure random string | `your-secure-random-string` |
|
||||
| `HOST` | Server host | `localhost` | `0.0.0.0` |
|
||||
| `PORT` | Server port | `4321` | `8080` |
|
||||
|
||||
### Important Security Note
|
||||
|
||||
The application will automatically generate a secure random `JWT_SECRET` on first run if one isn't provided or if the default value is used. This generated secret is stored in the data directory for persistence across container restarts.
|
||||
|
||||
While this auto-generation feature provides good security by default, you can still explicitly set your own `JWT_SECRET` for complete control over your deployment.
|
||||
|
||||
## Web UI Configuration
|
||||
|
||||
After installing and starting Gitea Mirror, you can configure it through the web interface:
|
||||
|
||||
1. Navigate to `http://your-server:port/`
|
||||
2. If this is your first time, you'll be guided through creating an admin account
|
||||
3. Log in with your credentials
|
||||
4. Go to the Configuration page
|
||||
|
||||
### GitHub Configuration
|
||||
|
||||
The GitHub configuration section allows you to connect to GitHub and specify which repositories to mirror.
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| Username | Your GitHub username | - |
|
||||
| Token | GitHub personal access token | - |
|
||||
| Skip Forks | Skip forked repositories | `false` |
|
||||
| Private Repositories | Include private repositories | `false` |
|
||||
| Mirror Issues | Mirror issues from GitHub to Gitea | `false` |
|
||||
| Mirror Wiki | Mirror wiki pages from GitHub to Gitea | `false` |
|
||||
| Mirror Starred | Mirror starred repositories | `false` |
|
||||
| Mirror Organizations | Mirror organization repositories | `false` |
|
||||
| Only Mirror Orgs | Only mirror organization repositories | `false` |
|
||||
| Preserve Org Structure | Maintain organization structure in Gitea | `false` |
|
||||
| Skip Starred Issues | Skip mirroring issues for starred repositories | `false` |
|
||||
|
||||
#### GitHub Token Permissions
|
||||
|
||||
Your GitHub token needs the following permissions:
|
||||
|
||||
- `repo` - Full control of private repositories
|
||||
- `read:org` - Read organization membership
|
||||
- `read:user` - Read user profile data
|
||||
|
||||
To create a GitHub token:
|
||||
|
||||
1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens)
|
||||
2. Click "Generate new token"
|
||||
3. Select the required permissions
|
||||
4. Copy the generated token and paste it into Gitea Mirror
|
||||
|
||||
### Gitea Configuration
|
||||
|
||||
The Gitea configuration section allows you to connect to your Gitea instance and specify how repositories should be mirrored.
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| URL | Gitea server URL | - |
|
||||
| Token | Gitea access token | - |
|
||||
| Organization | Default organization for mirrored repositories | - |
|
||||
| Visibility | Default visibility for mirrored repositories | `public` |
|
||||
| Starred Repos Org | Organization for starred repositories | `github` |
|
||||
|
||||
#### Gitea Token Creation
|
||||
|
||||
To create a Gitea access token:
|
||||
|
||||
1. Log in to your Gitea instance
|
||||
2. Go to Settings > Applications
|
||||
3. Under "Generate New Token", enter a name for your token
|
||||
4. Click "Generate Token"
|
||||
5. Copy the generated token and paste it into Gitea Mirror
|
||||
|
||||
### Schedule Configuration
|
||||
|
||||
You can configure automatic mirroring on a schedule:
|
||||
|
||||
| Option | Description | Default |
|
||||
|--------|-------------|---------|
|
||||
| Enable Scheduling | Enable automatic mirroring | `false` |
|
||||
| Interval (seconds) | Time between mirroring operations | `3600` (1 hour) |
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Repository Filtering
|
||||
|
||||
You can include or exclude specific repositories using patterns:
|
||||
|
||||
- Include patterns: Only repositories matching these patterns will be mirrored
|
||||
- Exclude patterns: Repositories matching these patterns will be skipped
|
||||
|
||||
Example patterns:
|
||||
- `*` - All repositories
|
||||
- `org-name/*` - All repositories in a specific organization
|
||||
- `username/repo-name` - A specific repository
|
||||
|
||||
### Database Management
|
||||
|
||||
Gitea Mirror includes several database management tools that can be run from the command line:
|
||||
|
||||
```bash
|
||||
# Initialize the database (only if it doesn't exist)
|
||||
bun run init-db
|
||||
|
||||
# Check database status
|
||||
bun run check-db
|
||||
|
||||
# Fix database location issues
|
||||
bun run fix-db
|
||||
|
||||
# Reset all users (for testing signup flow)
|
||||
bun run reset-users
|
||||
|
||||
# Remove database files completely
|
||||
bun run cleanup-db
|
||||
```
|
||||
|
||||
### Event Management
|
||||
|
||||
Events in Gitea Mirror (such as repository mirroring operations) are stored in the SQLite database. You can manage these events using the following scripts:
|
||||
|
||||
```bash
|
||||
# View all events in the database
|
||||
bun scripts/check-events.ts
|
||||
|
||||
# Mark all events as read
|
||||
bun scripts/mark-events-read.ts
|
||||
```
|
||||
|
||||
For cleaning up old activities and events, use the cleanup button in the Activity Log page of the web interface.
|
||||
|
||||
### Health Check Endpoint
|
||||
|
||||
Gitea Mirror includes a built-in health check endpoint at `/api/health` that provides:
|
||||
|
||||
- System status and uptime
|
||||
- Database connectivity check
|
||||
- Memory usage statistics
|
||||
- Environment information
|
||||
|
||||
You can use this endpoint for monitoring your deployment:
|
||||
|
||||
```bash
|
||||
# Basic check (returns 200 OK if healthy)
|
||||
curl -I http://your-server:port/api/health
|
||||
|
||||
# Detailed health information (JSON)
|
||||
curl http://your-server:port/api/health
|
||||
```
|
||||
@@ -1,182 +0,0 @@
|
||||
---
|
||||
title: "Quick Start Guide"
|
||||
description: "Get started with Gitea Mirror quickly."
|
||||
order: 3
|
||||
updatedDate: 2025-05-22
|
||||
---
|
||||
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-foreground">Gitea Mirror Quick Start Guide</h1>
|
||||
<p class="text-muted-foreground mt-2">This guide will help you get Gitea Mirror up and running quickly.</p>
|
||||
</div>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure you have:
|
||||
|
||||
1. <span class="font-semibold text-foreground">A GitHub account with a personal access token</span>
|
||||
2. <span class="font-semibold text-foreground">A Gitea instance with an access token</span>
|
||||
3. <span class="font-semibold text-foreground">One of the following:</span>
|
||||
- Docker and docker-compose (for Docker deployment)
|
||||
- Bun 1.2.9+ (for native deployment)
|
||||
- Proxmox VE or LXD (for LXC container deployment)
|
||||
|
||||
## Installation Options
|
||||
|
||||
Choose the installation method that works best for your environment.
|
||||
|
||||
### Using Docker (Recommended for most users)
|
||||
|
||||
Docker provides the easiest way to get started with minimal configuration.
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
```
|
||||
|
||||
2. Start the application in production mode:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
3. Access the application at [http://localhost:4321](http://localhost:4321)
|
||||
|
||||
### Using Bun (Native Installation)
|
||||
|
||||
If you prefer to run the application directly on your system:
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
```
|
||||
|
||||
2. Run the quick setup script:
|
||||
```bash
|
||||
bun run setup
|
||||
```
|
||||
This installs dependencies and initializes the database.
|
||||
|
||||
3. Choose how to run the application:
|
||||
|
||||
**Development Mode:**
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Note: For Bun-specific features, use:
|
||||
```bash
|
||||
bunx --bun astro dev
|
||||
```
|
||||
|
||||
**Production Mode:**
|
||||
```bash
|
||||
bun run build
|
||||
bun run start
|
||||
```
|
||||
|
||||
4. Access the application at [http://localhost:4321](http://localhost:4321)
|
||||
|
||||
### Using LXC Containers (Recommended for server deployments)
|
||||
|
||||
#### Proxmox VE (Online Installation)
|
||||
|
||||
For deploying on a Proxmox VE host with internet access:
|
||||
|
||||
```bash
|
||||
# Optional env overrides: CTID HOSTNAME STORAGE DISK_SIZE CORES MEMORY BRIDGE IP_CONF
|
||||
sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-proxmox.sh)"
|
||||
```
|
||||
|
||||
This script:
|
||||
- Creates a privileged LXC container
|
||||
- Installs Bun and dependencies
|
||||
- Clones and builds the application
|
||||
- Sets up a systemd service
|
||||
|
||||
#### Local LXD (Offline-friendly Installation)
|
||||
|
||||
For testing on a local workstation or in environments without internet access:
|
||||
|
||||
1. Clone the repository locally:
|
||||
```bash
|
||||
git clone https://github.com/arunavo4/gitea-mirror.git
|
||||
```
|
||||
|
||||
2. Download the Bun installer once:
|
||||
```bash
|
||||
curl -L -o /tmp/bun-linux-x64.zip https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip
|
||||
```
|
||||
|
||||
3. Run the local LXC installer:
|
||||
```bash
|
||||
sudo LOCAL_REPO_DIR=~/path/to/gitea-mirror ./gitea-mirror/scripts/gitea-mirror-lxc-local.sh
|
||||
```
|
||||
|
||||
For more details on LXC deployment, see the [LXC Container Deployment Guide](https://github.com/arunavo4/gitea-mirror/blob/main/scripts/README-lxc.md).
|
||||
|
||||
## Initial Configuration
|
||||
|
||||
Follow these steps to configure Gitea Mirror for first use:
|
||||
|
||||
1. **Create Admin Account**
|
||||
- Upon first access, you'll be prompted to create an admin account
|
||||
- Choose a secure username and password
|
||||
- This will be your administrator account
|
||||
|
||||
2. **Configure GitHub Connection**
|
||||
- Navigate to the Configuration page
|
||||
- Enter your GitHub username
|
||||
- Enter your GitHub personal access token
|
||||
- Select which repositories to mirror (all, starred, organizations)
|
||||
- Configure repository filtering options
|
||||
|
||||
3. **Configure Gitea Connection**
|
||||
- Enter your Gitea server URL
|
||||
- Enter your Gitea access token
|
||||
- Configure organization and visibility settings
|
||||
|
||||
4. **Set Up Scheduling (Optional)**
|
||||
- Enable automatic mirroring if desired
|
||||
- Set the mirroring interval (in seconds)
|
||||
|
||||
5. **Save Configuration**
|
||||
- Click the "Save" button to store your settings
|
||||
|
||||
## Performing Your First Mirror
|
||||
|
||||
After completing the configuration, you can start mirroring repositories:
|
||||
|
||||
1. Click "Import GitHub Data" to fetch repositories from GitHub
|
||||
2. Go to the Repositories page to view your imported repositories
|
||||
3. Select the repositories you want to mirror
|
||||
4. Click "Mirror Selected" to start the mirroring process
|
||||
5. Monitor the progress on the Activity page
|
||||
6. You'll receive toast notifications about the success or failure of operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any issues:
|
||||
|
||||
- Check the Activity Log for detailed error messages
|
||||
- Verify your GitHub and Gitea tokens have the correct permissions
|
||||
- Ensure your Gitea instance is accessible from the machine running Gitea Mirror
|
||||
- Check logs based on your deployment method:
|
||||
- Docker: `docker logs gitea-mirror`
|
||||
- Native: Check the terminal output or system logs
|
||||
- LXC: `systemctl status gitea-mirror` or `journalctl -u gitea-mirror -f`
|
||||
- Use the health check endpoint to verify system status: `curl http://your-server:4321/api/health`
|
||||
- For database issues, try the database management tools: `bun run check-db` or `bun run fix-db`
|
||||
|
||||
## Next Steps
|
||||
|
||||
After your initial setup:
|
||||
|
||||
- Explore the dashboard for an overview of your mirroring status
|
||||
- Set up automatic mirroring schedules for hands-off operation
|
||||
- Configure organization mirroring for team repositories
|
||||
- Check out the [Configuration Guide](/configuration) for advanced settings
|
||||
- Review the [Architecture Documentation](/architecture) to understand the system
|
||||
- For server deployments, set up monitoring using the health check endpoint
|
||||
- Use the cleanup button in the Activity Log page to manage old events and activities
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { mirrorApi } from '@/lib/api';
|
||||
import type { MirrorJob } from '@/lib/db/schema';
|
||||
|
||||
export function useMirror() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentJob, setCurrentJob] = useState<MirrorJob | null>(null);
|
||||
const [jobs, setJobs] = useState<MirrorJob[]>([]);
|
||||
|
||||
const startMirror = async (configId: string, repositoryIds?: string[]) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const job = await mirrorApi.startMirror(configId, repositoryIds);
|
||||
setCurrentJob(job);
|
||||
return job;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to start mirroring');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getMirrorJobs = async (configId: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const fetchedJobs = await mirrorApi.getMirrorJobs(configId);
|
||||
setJobs(fetchedJobs);
|
||||
return fetchedJobs;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch mirror jobs');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getMirrorJob = async (jobId: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const job = await mirrorApi.getMirrorJob(jobId);
|
||||
setCurrentJob(job);
|
||||
return job;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch mirror job');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelMirrorJob = async (jobId: string) => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await mirrorApi.cancelMirrorJob(jobId);
|
||||
if (result.success && currentJob?.id === jobId) {
|
||||
setCurrentJob({ ...currentJob, status: 'failed' });
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to cancel mirror job');
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
error,
|
||||
currentJob,
|
||||
jobs,
|
||||
startMirror,
|
||||
getMirrorJobs,
|
||||
getMirrorJob,
|
||||
cancelMirrorJob,
|
||||
};
|
||||
}
|
||||
@@ -28,11 +28,44 @@ try {
|
||||
|
||||
// Ensure all required tables exist
|
||||
ensureTablesExist(sqlite);
|
||||
|
||||
// Run migrations
|
||||
runMigrations(sqlite);
|
||||
} catch (error) {
|
||||
console.error("Error opening database:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run database migrations
|
||||
*/
|
||||
function runMigrations(db: Database) {
|
||||
try {
|
||||
// Migration 1: Add destination_org column to organizations table
|
||||
const orgTableInfo = db.query("PRAGMA table_info(organizations)").all() as Array<{name: string}>;
|
||||
const hasDestinationOrg = orgTableInfo.some(col => col.name === 'destination_org');
|
||||
|
||||
if (!hasDestinationOrg) {
|
||||
console.log("🔄 Running migration: Adding destination_org column to organizations table");
|
||||
db.exec("ALTER TABLE organizations ADD COLUMN destination_org TEXT");
|
||||
console.log("✅ Migration completed: destination_org column added");
|
||||
}
|
||||
|
||||
// Migration 2: Add destination_org column to repositories table
|
||||
const repoTableInfo = db.query("PRAGMA table_info(repositories)").all() as Array<{name: string}>;
|
||||
const hasRepoDestinationOrg = repoTableInfo.some(col => col.name === 'destination_org');
|
||||
|
||||
if (!hasRepoDestinationOrg) {
|
||||
console.log("🔄 Running migration: Adding destination_org column to repositories table");
|
||||
db.exec("ALTER TABLE repositories ADD COLUMN destination_org TEXT");
|
||||
console.log("✅ Migration completed: destination_org column added to repositories");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error running migrations:", error);
|
||||
// Don't throw - migrations should be non-breaking
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure all required tables exist in the database
|
||||
*/
|
||||
@@ -159,6 +192,7 @@ function createTable(db: Database, tableName: string) {
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||
destination_org TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
@@ -437,6 +471,9 @@ export const organizations = sqliteTable("organizations", {
|
||||
.notNull()
|
||||
.default(true),
|
||||
|
||||
// Override destination organization for this GitHub org's repos
|
||||
destinationOrg: text("destination_org"),
|
||||
|
||||
status: text("status").notNull().default("imported"),
|
||||
lastMirrored: integer("last_mirrored", { mode: "timestamp" }),
|
||||
errorMessage: text("error_message"),
|
||||
|
||||
@@ -44,7 +44,8 @@ export const configSchema = z.object({
|
||||
visibility: z.enum(["public", "private", "limited"]).default("public"),
|
||||
starredReposOrg: z.string().default("github"),
|
||||
preserveOrgStructure: z.boolean().default(false),
|
||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).optional(),
|
||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).optional(),
|
||||
personalReposOrg: z.string().optional(), // Override destination for personal repos
|
||||
}),
|
||||
include: z.array(z.string()).default(["*"]),
|
||||
exclude: z.array(z.string()).default([]),
|
||||
@@ -100,6 +101,7 @@ export const repositorySchema = z.object({
|
||||
errorMessage: z.string().optional(),
|
||||
|
||||
mirroredLocation: z.string().default(""), // Store the full Gitea path where repo was mirrored
|
||||
destinationOrg: z.string().optional(), // Custom destination organization override
|
||||
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
updatedAt: z.date().default(() => new Date()),
|
||||
@@ -158,6 +160,9 @@ export const organizationSchema = z.object({
|
||||
privateRepositoryCount: z.number().optional(),
|
||||
forkRepositoryCount: z.number().optional(),
|
||||
|
||||
// Override destination organization for this GitHub org's repos
|
||||
destinationOrg: z.string().optional(),
|
||||
|
||||
createdAt: z.date().default(() => new Date()),
|
||||
updatedAt: z.date().default(() => new Date()),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { repoStatusEnum } from "@/types/Repository";
|
||||
import { getOrCreateGiteaOrg } from "./gitea";
|
||||
import { getOrCreateGiteaOrg, getGiteaRepoOwner, getGiteaRepoOwnerAsync } from "./gitea";
|
||||
import type { Config, Repository, Organization } from "./db/schema";
|
||||
|
||||
// Mock the isRepoPresentInGitea function
|
||||
const mockIsRepoPresentInGitea = mock(() => Promise.resolve(false));
|
||||
@@ -291,3 +292,133 @@ describe("Gitea Repository Mirroring", () => {
|
||||
expect(mockGetOrCreateGiteaOrg).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
const baseConfig: Partial<Config> = {
|
||||
githubConfig: {
|
||||
username: "testuser",
|
||||
token: "token",
|
||||
preserveOrgStructure: false,
|
||||
skipForks: false,
|
||||
privateRepositories: false,
|
||||
mirrorIssues: false,
|
||||
mirrorWiki: false,
|
||||
mirrorStarred: false,
|
||||
useSpecificUser: false,
|
||||
includeOrgs: [],
|
||||
excludeOrgs: [],
|
||||
mirrorPublicOrgs: false,
|
||||
publicOrgs: [],
|
||||
skipStarredIssues: false
|
||||
},
|
||||
giteaConfig: {
|
||||
username: "giteauser",
|
||||
url: "https://gitea.example.com",
|
||||
token: "gitea-token",
|
||||
organization: "github-mirrors",
|
||||
visibility: "public",
|
||||
starredReposOrg: "starred",
|
||||
preserveOrgStructure: false,
|
||||
mirrorStrategy: "preserve"
|
||||
}
|
||||
};
|
||||
|
||||
const baseRepo: Repository = {
|
||||
id: "repo-id",
|
||||
userId: "user-id",
|
||||
configId: "config-id",
|
||||
name: "test-repo",
|
||||
fullName: "testuser/test-repo",
|
||||
url: "https://github.com/testuser/test-repo",
|
||||
cloneUrl: "https://github.com/testuser/test-repo.git",
|
||||
owner: "testuser",
|
||||
isPrivate: false,
|
||||
isForked: false,
|
||||
hasIssues: true,
|
||||
isStarred: false,
|
||||
isArchived: false,
|
||||
size: 1000,
|
||||
hasLFS: false,
|
||||
hasSubmodules: false,
|
||||
defaultBranch: "main",
|
||||
visibility: "public",
|
||||
status: "imported",
|
||||
mirroredLocation: "",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
test("starred repos go to starredReposOrg", () => {
|
||||
const repo = { ...baseRepo, isStarred: true };
|
||||
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||
expect(result).toBe("starred");
|
||||
});
|
||||
|
||||
test("preserve strategy: personal repos use personalReposOrg override", () => {
|
||||
const configWithOverride = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
...baseConfig.giteaConfig!,
|
||||
personalReposOrg: "my-personal-mirrors"
|
||||
}
|
||||
};
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: configWithOverride, repository: repo });
|
||||
expect(result).toBe("my-personal-mirrors");
|
||||
});
|
||||
|
||||
test("preserve strategy: personal repos fallback to username when no override", () => {
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||
expect(result).toBe("giteauser");
|
||||
});
|
||||
|
||||
test("preserve strategy: org repos go to same org name", () => {
|
||||
const repo = { ...baseRepo, organization: "myorg" };
|
||||
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||
expect(result).toBe("myorg");
|
||||
});
|
||||
|
||||
test("mixed strategy: personal repos go to organization", () => {
|
||||
const configWithMixed = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
...baseConfig.giteaConfig!,
|
||||
mirrorStrategy: "mixed" as const,
|
||||
organization: "github-mirrors"
|
||||
}
|
||||
};
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||
expect(result).toBe("github-mirrors");
|
||||
});
|
||||
|
||||
test("mixed strategy: org repos preserve their structure", () => {
|
||||
const configWithMixed = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
...baseConfig.giteaConfig!,
|
||||
mirrorStrategy: "mixed" as const,
|
||||
organization: "github-mirrors"
|
||||
}
|
||||
};
|
||||
const repo = { ...baseRepo, organization: "myorg" };
|
||||
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||
expect(result).toBe("myorg");
|
||||
});
|
||||
|
||||
test("mixed strategy: fallback to username if no org configs", () => {
|
||||
const configWithMixed = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
...baseConfig.giteaConfig!,
|
||||
mirrorStrategy: "mixed" as const,
|
||||
organization: undefined,
|
||||
personalReposOrg: undefined
|
||||
}
|
||||
};
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||
expect(result).toBe("giteauser");
|
||||
});
|
||||
});
|
||||
|
||||
240
src/lib/gitea.ts
240
src/lib/gitea.ts
@@ -9,7 +9,87 @@ import type { Organization, Repository } from "./db/schema";
|
||||
import { httpPost, httpGet } from "./http-client";
|
||||
import { createMirrorJob } from "./helpers";
|
||||
import { db, organizations, repositories } from "./db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Helper function to get organization configuration including destination override
|
||||
*/
|
||||
export const getOrganizationConfig = async ({
|
||||
orgName,
|
||||
userId,
|
||||
}: {
|
||||
orgName: string;
|
||||
userId: string;
|
||||
}): Promise<Organization | null> => {
|
||||
try {
|
||||
const [orgConfig] = await db
|
||||
.select()
|
||||
.from(organizations)
|
||||
.where(and(eq(organizations.name, orgName), eq(organizations.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
return orgConfig || null;
|
||||
} catch (error) {
|
||||
console.error(`Error fetching organization config for ${orgName}:`, error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Enhanced async version of getGiteaRepoOwner that supports organization overrides
|
||||
*/
|
||||
export const getGiteaRepoOwnerAsync = async ({
|
||||
config,
|
||||
repository,
|
||||
}: {
|
||||
config: Partial<Config>;
|
||||
repository: Repository;
|
||||
}): Promise<string> => {
|
||||
if (!config.githubConfig || !config.giteaConfig) {
|
||||
throw new Error("GitHub or Gitea config is required.");
|
||||
}
|
||||
|
||||
if (!config.giteaConfig.username) {
|
||||
throw new Error("Gitea username is required.");
|
||||
}
|
||||
|
||||
if (!config.userId) {
|
||||
throw new Error("User ID is required for organization overrides.");
|
||||
}
|
||||
|
||||
// Check if repository is starred - starred repos always go to starredReposOrg (highest priority)
|
||||
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
||||
return config.giteaConfig.starredReposOrg;
|
||||
}
|
||||
|
||||
// Check for repository-specific override (second highest priority)
|
||||
if (repository.destinationOrg) {
|
||||
console.log(`Using repository override: ${repository.fullName} -> ${repository.destinationOrg}`);
|
||||
return repository.destinationOrg;
|
||||
}
|
||||
|
||||
// Check for organization-specific override
|
||||
if (repository.organization) {
|
||||
const orgConfig = await getOrganizationConfig({
|
||||
orgName: repository.organization,
|
||||
userId: config.userId,
|
||||
});
|
||||
|
||||
if (orgConfig?.destinationOrg) {
|
||||
console.log(`Using organization override: ${repository.organization} -> ${orgConfig.destinationOrg}`);
|
||||
return orgConfig.destinationOrg;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for personal repos override (when it's user's repo, not an organization)
|
||||
if (!repository.organization && config.giteaConfig.personalReposOrg) {
|
||||
console.log(`Using personal repos override: ${config.giteaConfig.personalReposOrg}`);
|
||||
return config.giteaConfig.personalReposOrg;
|
||||
}
|
||||
|
||||
// Fall back to existing strategy logic
|
||||
return getGiteaRepoOwner({ config, repository });
|
||||
};
|
||||
|
||||
export const getGiteaRepoOwner = ({
|
||||
config,
|
||||
@@ -26,13 +106,53 @@ export const getGiteaRepoOwner = ({
|
||||
throw new Error("Gitea username is required.");
|
||||
}
|
||||
|
||||
// if the config has preserveOrgStructure set to true, then use the org name as the owner
|
||||
if (config.githubConfig.preserveOrgStructure && repository.organization) {
|
||||
return repository.organization;
|
||||
// Check if repository is starred - starred repos always go to starredReposOrg
|
||||
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
||||
return config.giteaConfig.starredReposOrg;
|
||||
}
|
||||
|
||||
// if the config has preserveOrgStructure set to false, then use the gitea username as the owner
|
||||
return config.giteaConfig.username;
|
||||
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
||||
const mirrorStrategy = config.giteaConfig.mirrorStrategy ||
|
||||
(config.githubConfig.preserveOrgStructure ? "preserve" : "flat-user");
|
||||
|
||||
switch (mirrorStrategy) {
|
||||
case "preserve":
|
||||
// Keep GitHub structure - org repos go to same org, personal repos to user (or override)
|
||||
if (repository.organization) {
|
||||
return repository.organization;
|
||||
}
|
||||
// Use personal repos override if configured, otherwise use username
|
||||
return config.giteaConfig.personalReposOrg || config.giteaConfig.username;
|
||||
|
||||
case "single-org":
|
||||
// All non-starred repos go to the destination organization
|
||||
if (config.giteaConfig.organization) {
|
||||
return config.giteaConfig.organization;
|
||||
}
|
||||
// Fallback to username if no organization specified
|
||||
return config.giteaConfig.username;
|
||||
|
||||
case "flat-user":
|
||||
// All non-starred repos go under the user account
|
||||
return config.giteaConfig.username;
|
||||
|
||||
case "mixed":
|
||||
// Mixed mode: personal repos to single org, organization repos preserve structure
|
||||
if (repository.organization) {
|
||||
// Organization repos preserve their structure
|
||||
return repository.organization;
|
||||
}
|
||||
// Personal repos go to configured organization (same as single-org)
|
||||
if (config.giteaConfig.organization) {
|
||||
return config.giteaConfig.organization;
|
||||
}
|
||||
// Fallback to username if no organization specified
|
||||
return config.giteaConfig.username;
|
||||
|
||||
default:
|
||||
// Default fallback
|
||||
return config.giteaConfig.username;
|
||||
}
|
||||
};
|
||||
|
||||
export const isRepoPresentInGitea = async ({
|
||||
@@ -134,15 +254,18 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
throw new Error("Gitea username is required.");
|
||||
}
|
||||
|
||||
// Get the correct owner based on the strategy (with organization overrides)
|
||||
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||
|
||||
const isExisting = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: config.giteaConfig.username,
|
||||
owner: repoOwner,
|
||||
repoName: repository.name,
|
||||
});
|
||||
|
||||
if (isExisting) {
|
||||
console.log(
|
||||
`Repository ${repository.name} already exists in Gitea. Updating database status.`
|
||||
`Repository ${repository.name} already exists in Gitea under ${repoOwner}. Updating database status.`
|
||||
);
|
||||
|
||||
// Update database to reflect that the repository is already mirrored
|
||||
@@ -153,7 +276,7 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
updatedAt: new Date(),
|
||||
lastMirrored: new Date(),
|
||||
errorMessage: null,
|
||||
mirroredLocation: `${config.giteaConfig.username}/${repository.name}`,
|
||||
mirroredLocation: `${repoOwner}/${repository.name}`,
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
@@ -163,7 +286,7 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
repositoryId: repository.id,
|
||||
repositoryName: repository.name,
|
||||
message: `Repository ${repository.name} already exists in Gitea`,
|
||||
details: `Repository ${repository.name} was found to already exist in Gitea and database status was updated.`,
|
||||
details: `Repository ${repository.name} was found to already exist in Gitea under ${repoOwner} and database status was updated.`,
|
||||
status: "mirrored",
|
||||
});
|
||||
|
||||
@@ -212,6 +335,15 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
|
||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||
|
||||
// Handle organization creation if needed for single-org or preserve strategies
|
||||
if (repoOwner !== config.giteaConfig.username && !repository.isStarred) {
|
||||
// Need to create the organization if it doesn't exist
|
||||
await getOrCreateGiteaOrg({
|
||||
orgName: repoOwner,
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await httpPost(
|
||||
apiUrl,
|
||||
{
|
||||
@@ -220,7 +352,7 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
mirror: true,
|
||||
wiki: config.githubConfig.mirrorWiki || false, // will mirror wiki if it exists
|
||||
private: repository.isPrivate,
|
||||
repo_owner: config.giteaConfig.username,
|
||||
repo_owner: repoOwner,
|
||||
description: "",
|
||||
service: "git",
|
||||
},
|
||||
@@ -237,7 +369,11 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
});
|
||||
|
||||
// clone issues
|
||||
if (config.githubConfig.mirrorIssues) {
|
||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||
const shouldMirrorIssues = config.githubConfig.mirrorIssues &&
|
||||
!(repository.isStarred && config.githubConfig.skipStarredIssues);
|
||||
|
||||
if (shouldMirrorIssues) {
|
||||
await mirrorGitRepoIssuesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
@@ -256,7 +392,7 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
updatedAt: new Date(),
|
||||
lastMirrored: new Date(),
|
||||
errorMessage: null,
|
||||
mirroredLocation: `${config.giteaConfig.username}/${repository.name}`,
|
||||
mirroredLocation: `${repoOwner}/${repository.name}`,
|
||||
})
|
||||
.where(eq(repositories.id, repository.id!));
|
||||
|
||||
@@ -390,7 +526,7 @@ export async function getOrCreateGiteaOrg({
|
||||
username: orgName,
|
||||
full_name: `${orgName} Org`,
|
||||
description: `Mirrored organization from GitHub ${orgName}`,
|
||||
visibility: "public",
|
||||
visibility: config.giteaConfig?.visibility || "public",
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -582,7 +718,11 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
});
|
||||
|
||||
// Clone issues
|
||||
if (config.githubConfig?.mirrorIssues) {
|
||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||
const shouldMirrorIssues = config.githubConfig?.mirrorIssues &&
|
||||
!(repository.isStarred && config.githubConfig?.skipStarredIssues);
|
||||
|
||||
if (shouldMirrorIssues) {
|
||||
await mirrorGitRepoIssuesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
@@ -729,11 +869,37 @@ export async function mirrorGitHubOrgToGitea({
|
||||
status: repoStatusEnum.parse("mirroring"),
|
||||
});
|
||||
|
||||
const giteaOrgId = await getOrCreateGiteaOrg({
|
||||
orgId: organization.id,
|
||||
orgName: organization.name,
|
||||
config,
|
||||
});
|
||||
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
||||
const mirrorStrategy = config.giteaConfig?.mirrorStrategy ||
|
||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||
|
||||
let giteaOrgId: number;
|
||||
let targetOrgName: string;
|
||||
|
||||
// Determine the target organization based on strategy
|
||||
if (mirrorStrategy === "single-org" && config.giteaConfig?.organization) {
|
||||
// For single-org strategy, use the configured destination organization
|
||||
targetOrgName = config.giteaConfig.organization;
|
||||
giteaOrgId = await getOrCreateGiteaOrg({
|
||||
orgId: organization.id,
|
||||
orgName: targetOrgName,
|
||||
config,
|
||||
});
|
||||
console.log(`Using single organization strategy: all repos will go to ${targetOrgName}`);
|
||||
} else if (mirrorStrategy === "preserve") {
|
||||
// For preserve strategy, create/use an org with the same name as GitHub
|
||||
targetOrgName = organization.name;
|
||||
giteaOrgId = await getOrCreateGiteaOrg({
|
||||
orgId: organization.id,
|
||||
orgName: targetOrgName,
|
||||
config,
|
||||
});
|
||||
} else {
|
||||
// For flat-user strategy, we shouldn't create organizations at all
|
||||
// Skip organization creation and let individual repos be handled by getGiteaRepoOwner
|
||||
console.log(`Using flat-user strategy: repos will be placed under user account`);
|
||||
targetOrgName = config.giteaConfig?.username || "";
|
||||
}
|
||||
|
||||
//query the db with the org name and get the repos
|
||||
const orgRepos = await db
|
||||
@@ -771,17 +937,27 @@ export async function mirrorGitHubOrgToGitea({
|
||||
|
||||
// Log the start of mirroring
|
||||
console.log(
|
||||
`Starting mirror for repository: ${repo.name} in organization ${organization.name}`
|
||||
`Starting mirror for repository: ${repo.name} from GitHub org ${organization.name}`
|
||||
);
|
||||
|
||||
// Mirror the repository
|
||||
await mirrorGitHubRepoToGiteaOrg({
|
||||
octokit,
|
||||
config,
|
||||
repository: repoData,
|
||||
giteaOrgId,
|
||||
orgName: organization.name,
|
||||
});
|
||||
// Mirror the repository based on strategy
|
||||
if (mirrorStrategy === "flat-user") {
|
||||
// For flat-user strategy, mirror directly to user account
|
||||
await mirrorGithubRepoToGitea({
|
||||
octokit,
|
||||
repository: repoData,
|
||||
config,
|
||||
});
|
||||
} else {
|
||||
// For preserve and single-org strategies, use organization
|
||||
await mirrorGitHubRepoToGiteaOrg({
|
||||
octokit,
|
||||
config,
|
||||
repository: repoData,
|
||||
giteaOrgId: giteaOrgId!,
|
||||
orgName: targetOrgName,
|
||||
});
|
||||
}
|
||||
|
||||
return repo;
|
||||
},
|
||||
@@ -905,8 +1081,8 @@ export const syncGiteaRepo = async ({
|
||||
status: repoStatusEnum.parse("syncing"),
|
||||
});
|
||||
|
||||
// Get the expected owner based on current config
|
||||
const repoOwner = getGiteaRepoOwner({ config, repository });
|
||||
// Get the expected owner based on current config (with organization overrides)
|
||||
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||
|
||||
// Check if repo exists at the expected location or alternate location
|
||||
const { present, actualOwner } = await checkRepoLocation({
|
||||
@@ -1207,7 +1383,7 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
throw new Error("Gitea config is incomplete for mirroring releases.");
|
||||
}
|
||||
|
||||
const repoOwner = getGiteaRepoOwner({
|
||||
const repoOwner = await getGiteaRepoOwnerAsync({
|
||||
config,
|
||||
repository,
|
||||
});
|
||||
|
||||
@@ -35,7 +35,8 @@ interface DbGiteaConfig {
|
||||
visibility: "public" | "private" | "limited";
|
||||
starredReposOrg: string;
|
||||
preserveOrgStructure: boolean;
|
||||
mirrorStrategy?: "preserve" | "single-org" | "flat-user";
|
||||
mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed";
|
||||
personalReposOrg?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,6 +107,7 @@ export function mapDbToUiConfig(dbConfig: any): {
|
||||
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
|
||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
|
||||
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
|
||||
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg,
|
||||
};
|
||||
|
||||
const mirrorOptions: MirrorOptions = {
|
||||
|
||||
37
src/pages/404.astro
Normal file
37
src/pages/404.astro
Normal file
@@ -0,0 +1,37 @@
|
||||
---
|
||||
import '../styles/global.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { NotFound } from '@/components/NotFound';
|
||||
|
||||
const generator = Astro.generator;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={generator} />
|
||||
<title>Page Not Found - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
<body>
|
||||
<NotFound client:load />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
/* Floating animation for 404 text */
|
||||
:global(.animate-float) {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -58,7 +58,7 @@ export const GET: APIRoute = async () => {
|
||||
latestVersion: latestVersion,
|
||||
updateAvailable: latestVersion !== "unknown" &&
|
||||
currentVersion !== "unknown" &&
|
||||
latestVersion !== currentVersion,
|
||||
compareVersions(currentVersion, latestVersion) < 0,
|
||||
database: dbStatus,
|
||||
recovery: recoveryStatus,
|
||||
system: systemInfo,
|
||||
@@ -174,6 +174,28 @@ function formatBytes(bytes: number): string {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare semantic versions
|
||||
* Returns:
|
||||
* -1 if v1 < v2
|
||||
* 0 if v1 = v2
|
||||
* 1 if v1 > v2
|
||||
*/
|
||||
function compareVersions(v1: string, v2: string): number {
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const part1 = parts1[i] || 0;
|
||||
const part2 = parts2[i] || 0;
|
||||
|
||||
if (part1 < part2) return -1;
|
||||
if (part1 > part2) return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the latest version from GitHub releases
|
||||
*/
|
||||
@@ -186,7 +208,7 @@ async function checkLatestVersion(): Promise<string> {
|
||||
try {
|
||||
// Fetch the latest release from GitHub
|
||||
const response = await httpGet(
|
||||
'https://api.github.com/repos/arunavo4/gitea-mirror/releases/latest',
|
||||
'https://api.github.com/repos/RayLabsHQ/gitea-mirror/releases/latest',
|
||||
{ 'Accept': 'application/vnd.github.v3+json' }
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||
import {
|
||||
mirrorGithubRepoToGitea,
|
||||
mirrorGitHubOrgRepoToGiteaOrg,
|
||||
getGiteaRepoOwnerAsync,
|
||||
} from "@/lib/gitea";
|
||||
import { createGitHubClient } from "@/lib/github";
|
||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||
@@ -96,12 +97,29 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
// Log the start of mirroring
|
||||
console.log(`Starting mirror for repository: ${repo.name}`);
|
||||
|
||||
// Mirror the repository based on whether it's in an organization
|
||||
if (repo.organization && config.githubConfig.preserveOrgStructure) {
|
||||
// Determine where the repository should be mirrored (with organization overrides)
|
||||
const owner = await getGiteaRepoOwnerAsync({
|
||||
config,
|
||||
repository: repoData,
|
||||
});
|
||||
|
||||
console.log(`Repository ${repo.name} will be mirrored to owner: ${owner}`);
|
||||
|
||||
// For single-org and starred repos strategies, or when mirroring to an org,
|
||||
// always use the org mirroring function to ensure proper organization handling
|
||||
const mirrorStrategy = config.giteaConfig?.mirrorStrategy ||
|
||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||
|
||||
const shouldUseOrgMirror =
|
||||
owner !== config.giteaConfig?.username || // Different owner means org
|
||||
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
||||
repoData.isStarred; // Starred repos always go to org
|
||||
|
||||
if (shouldUseOrgMirror) {
|
||||
await mirrorGitHubOrgRepoToGiteaOrg({
|
||||
config,
|
||||
octokit,
|
||||
orgName: repo.organization,
|
||||
orgName: owner,
|
||||
repository: repoData,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, configs, repositories } from "@/lib/db";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { getGiteaRepoOwner, isRepoPresentInGitea } from "@/lib/gitea";
|
||||
import { getGiteaRepoOwnerAsync, isRepoPresentInGitea } from "@/lib/gitea";
|
||||
import {
|
||||
mirrorGithubRepoToGitea,
|
||||
mirrorGitHubOrgRepoToGiteaOrg,
|
||||
@@ -93,6 +93,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
lastMirrored: repo.lastMirrored ?? undefined,
|
||||
errorMessage: repo.errorMessage ?? undefined,
|
||||
forkedFrom: repo.forkedFrom ?? undefined,
|
||||
mirroredLocation: repo.mirroredLocation || "",
|
||||
};
|
||||
|
||||
// Log the start of retry operation
|
||||
@@ -108,8 +109,8 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
status: "imported",
|
||||
});
|
||||
|
||||
// Determine if the repository exists in Gitea
|
||||
let owner = getGiteaRepoOwner({
|
||||
// Determine if the repository exists in Gitea (with organization overrides)
|
||||
let owner = await getGiteaRepoOwnerAsync({
|
||||
config,
|
||||
repository: repoData,
|
||||
});
|
||||
@@ -134,13 +135,23 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
throw new Error("Octokit client is not initialized.");
|
||||
}
|
||||
|
||||
console.log(`Importing repo: ${repo.name} ${owner}`);
|
||||
console.log(`Importing repo: ${repo.name} to owner: ${owner}`);
|
||||
|
||||
if (repo.organization && config.githubConfig.preserveOrgStructure) {
|
||||
// For single-org and starred repos strategies, or when mirroring to an org,
|
||||
// always use the org mirroring function to ensure proper organization handling
|
||||
const mirrorStrategy = config.giteaConfig?.mirrorStrategy ||
|
||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||
|
||||
const shouldUseOrgMirror =
|
||||
owner !== config.giteaConfig?.username || // Different owner means org
|
||||
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
||||
repoData.isStarred; // Starred repos always go to org
|
||||
|
||||
if (shouldUseOrgMirror) {
|
||||
await mirrorGitHubOrgRepoToGiteaOrg({
|
||||
config,
|
||||
octokit,
|
||||
orgName: repo.organization,
|
||||
orgName: owner,
|
||||
repository: {
|
||||
...repoData,
|
||||
status: repoStatusEnum.parse("imported"),
|
||||
|
||||
@@ -112,6 +112,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
organization: repo.organization ?? undefined,
|
||||
lastMirrored: repo.lastMirrored ?? undefined,
|
||||
errorMessage: repo.errorMessage ?? undefined,
|
||||
mirroredLocation: repo.mirroredLocation || "",
|
||||
forkedFrom: repo.forkedFrom ?? undefined,
|
||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||
},
|
||||
@@ -133,6 +134,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
errorMessage: repo.errorMessage ?? undefined,
|
||||
forkedFrom: repo.forkedFrom ?? undefined,
|
||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||
mirroredLocation: repo.mirroredLocation || "",
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { repositoryVisibilityEnum, repoStatusEnum } from "@/types/Repository";
|
||||
import { syncGiteaRepo } from "@/lib/gitea";
|
||||
import type { SyncRepoResponse } from "@/types/sync";
|
||||
import { processWithResilience } from "@/lib/utils/concurrency";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
@@ -68,9 +67,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
// Define the concurrency limit - adjust based on API rate limits
|
||||
const CONCURRENCY_LIMIT = 5;
|
||||
|
||||
// Generate a batch ID to group related repositories
|
||||
const batchId = uuidv4();
|
||||
|
||||
// Process repositories in parallel with resilience to container restarts
|
||||
await processWithResilience(
|
||||
repos,
|
||||
@@ -84,6 +80,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
errorMessage: repo.errorMessage ?? undefined,
|
||||
forkedFrom: repo.forkedFrom ?? undefined,
|
||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||
mirroredLocation: repo.mirroredLocation || "",
|
||||
};
|
||||
|
||||
// Log the start of syncing
|
||||
@@ -100,7 +97,6 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
{
|
||||
userId: config.userId || "",
|
||||
jobType: "sync",
|
||||
batchId,
|
||||
getItemId: (repo) => repo.id,
|
||||
getItemName: (repo) => repo.name,
|
||||
concurrencyLimit: CONCURRENCY_LIMIT,
|
||||
@@ -135,6 +131,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
errorMessage: repo.errorMessage ?? undefined,
|
||||
forkedFrom: repo.forkedFrom ?? undefined,
|
||||
visibility: repositoryVisibilityEnum.parse(repo.visibility),
|
||||
mirroredLocation: repo.mirroredLocation || "",
|
||||
})),
|
||||
};
|
||||
|
||||
|
||||
82
src/pages/api/organizations/[id].ts
Normal file
82
src/pages/api/organizations/[id].ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, organizations } from "@/lib/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||
try {
|
||||
// Get token from Authorization header or cookies
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||
|
||||
if (!token) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token and get user ID
|
||||
let userId: string;
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||
userId = decoded.id;
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const orgId = params.id;
|
||||
if (!orgId) {
|
||||
return new Response(JSON.stringify({ error: "Organization ID is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { destinationOrg } = body;
|
||||
|
||||
// Validate that the organization belongs to the user
|
||||
const [existingOrg] = await db
|
||||
.select()
|
||||
.from(organizations)
|
||||
.where(and(eq(organizations.id, orgId), eq(organizations.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingOrg) {
|
||||
return new Response(JSON.stringify({ error: "Organization not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Update the organization's destination override
|
||||
await db
|
||||
.update(organizations)
|
||||
.set({
|
||||
destinationOrg: destinationOrg || null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(organizations.id, orgId));
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: "Organization destination updated successfully",
|
||||
destinationOrg: destinationOrg || null,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "Update organization destination", 500);
|
||||
}
|
||||
};
|
||||
82
src/pages/api/repositories/[id].ts
Normal file
82
src/pages/api/repositories/[id].ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { db, repositories } from "@/lib/db";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { createSecureErrorResponse } from "@/lib/utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
|
||||
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
||||
try {
|
||||
// Get token from Authorization header or cookies
|
||||
const authHeader = request.headers.get("Authorization");
|
||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
||||
|
||||
if (!token) {
|
||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Verify token and get user ID
|
||||
let userId: string;
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
||||
userId = decoded.id;
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const repoId = params.id;
|
||||
if (!repoId) {
|
||||
return new Response(JSON.stringify({ error: "Repository ID is required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { destinationOrg } = body;
|
||||
|
||||
// Validate that the repository belongs to the user
|
||||
const [existingRepo] = await db
|
||||
.select()
|
||||
.from(repositories)
|
||||
.where(and(eq(repositories.id, repoId), eq(repositories.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (!existingRepo) {
|
||||
return new Response(JSON.stringify({ error: "Repository not found" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
// Update the repository's destination override
|
||||
await db
|
||||
.update(repositories)
|
||||
.set({
|
||||
destinationOrg: destinationOrg || null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(repositories.id, repoId));
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: "Repository destination updated successfully",
|
||||
destinationOrg: destinationOrg || null,
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error, "Update repository destination", 500);
|
||||
}
|
||||
};
|
||||
@@ -97,6 +97,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
status: "imported" as Repository["status"],
|
||||
lastMirrored: undefined,
|
||||
errorMessage: undefined,
|
||||
mirroredLocation: "",
|
||||
createdAt: repoData.created_at
|
||||
? new Date(repoData.created_at)
|
||||
: new Date(),
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
|
||||
// Enable prerendering for this dynamic route
|
||||
export const prerender = true;
|
||||
|
||||
// Generate static paths for all documentation pages
|
||||
export async function getStaticPaths() {
|
||||
const docs = await getCollection('docs');
|
||||
return docs.map(entry => ({
|
||||
params: { slug: entry.slug },
|
||||
props: { entry },
|
||||
}));
|
||||
}
|
||||
|
||||
// Get the documentation entry from props
|
||||
const { entry } = Astro.props;
|
||||
const { Content } = await entry.render();
|
||||
---
|
||||
|
||||
<MainLayout title={entry.data.title}>
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
</a>
|
||||
</div>
|
||||
<article class="bg-card rounded-2xl shadow-lg p-6 border border-border">
|
||||
<div class="prose prose-neutral dark:prose-invert prose-code:bg-muted prose-code:text-foreground prose-pre:bg-muted prose-pre:text-foreground prose-pre:rounded-lg prose-pre:p-4 prose-table:rounded-lg prose-table:bg-muted prose-th:text-foreground prose-td:text-muted-foreground prose-blockquote:border-l-4 prose-blockquote:border-muted prose-blockquote:bg-muted/50 prose-blockquote:p-4">
|
||||
<Content />
|
||||
</div>
|
||||
</article>
|
||||
<script type="module">
|
||||
// Mermaid diagram rendering for code blocks
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
||||
mermaid.initialize({ startOnLoad: false, theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default' });
|
||||
function renderMermaidDiagrams() {
|
||||
document.querySelectorAll('pre code.language-mermaid').forEach((block, i) => {
|
||||
const parent = block.parentElement;
|
||||
if (!parent) return;
|
||||
const code = block.textContent;
|
||||
const id = `mermaid-diagram-${i}`;
|
||||
const container = document.createElement('div');
|
||||
container.className = 'my-6';
|
||||
container.id = id;
|
||||
parent.replaceWith(container);
|
||||
mermaid.render(id, code, (svgCode) => {
|
||||
container.innerHTML = svgCode;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', renderMermaidDiagrams);
|
||||
} else {
|
||||
renderMermaidDiagrams();
|
||||
}
|
||||
</script>
|
||||
</main>
|
||||
</MainLayout>
|
||||
335
src/pages/docs/architecture.astro
Normal file
335
src/pages/docs/architecture.astro
Normal file
@@ -0,0 +1,335 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
---
|
||||
|
||||
<MainLayout title="Architecture - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<article class="bg-card rounded-2xl shadow-lg p-6 md:p-8 border border-border">
|
||||
<!-- Header -->
|
||||
<div class="mb-12 space-y-4">
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-1 4h1m-1 4h1"/>
|
||||
</svg>
|
||||
<span>Architecture Overview</span>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Gitea Mirror Architecture</h1>
|
||||
<p class="text-lg text-muted-foreground leading-relaxed max-w-4xl">
|
||||
This document provides a comprehensive overview of the Gitea Mirror application architecture, including component diagrams, project structure, and detailed explanations of each part of the system.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- System Overview -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">System Overview</h2>
|
||||
|
||||
<div class="bg-card/50 border border-border/50 rounded-lg p-6 mb-8">
|
||||
<p class="text-base leading-relaxed mb-6">
|
||||
Gitea Mirror is a web application that automates the mirroring of GitHub repositories to Gitea instances. It provides a user-friendly interface for configuring, monitoring, and managing mirroring operations without requiring users to edit configuration files or run Docker commands.
|
||||
</p>
|
||||
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">Technology Stack</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{[
|
||||
{ name: 'Astro', desc: 'Web framework for Server-Side Rendering (SSR)' },
|
||||
{ name: 'React', desc: 'Component library for interactive UI elements' },
|
||||
{ name: 'Tailwind CSS v4', desc: 'Utility-first CSS framework (with Vite plugin)' },
|
||||
{ name: 'Shadcn UI', desc: 'UI component library built on Tailwind CSS' },
|
||||
{ name: 'SQLite', desc: 'Database for storing configuration, state, and events' },
|
||||
{ name: 'Bun', desc: 'JavaScript runtime and package manager' },
|
||||
{ name: 'Drizzle ORM', desc: 'Type-safe ORM for database interactions' }
|
||||
].map(tech => (
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-2 h-2 rounded-full bg-primary mt-2"></div>
|
||||
<div>
|
||||
<span class="font-semibold text-foreground">{tech.name}</span>
|
||||
<p class="text-sm text-muted-foreground">{tech.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Architecture Diagram -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Architecture Diagram</h2>
|
||||
|
||||
<div class="my-8">
|
||||
<div class="architecture-diagram bg-muted/30 rounded-xl p-8 border border-border">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Gitea Mirror System -->
|
||||
<div class="bg-card rounded-lg border-2 border-primary/20 p-6">
|
||||
<h3 class="text-lg font-semibold mb-6 text-center text-primary">Gitea Mirror System</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
{[
|
||||
{ icon: '🎨', name: 'Frontend', tech: 'Astro + React' },
|
||||
{ icon: '⚙️', name: 'Backend', tech: 'Bun Runtime' },
|
||||
{ icon: '🗄️', name: 'Database', tech: 'SQLite + Drizzle' }
|
||||
].map((component, index) => (
|
||||
<>
|
||||
<div class="bg-primary/10 rounded-lg p-4 text-center">
|
||||
<div class="text-2xl mb-2">{component.icon}</div>
|
||||
<h4 class="font-semibold">{component.name}</h4>
|
||||
<p class="text-sm text-muted-foreground mt-1">{component.tech}</p>
|
||||
</div>
|
||||
{index < 2 && (
|
||||
<div class="flex justify-center">
|
||||
<div class="text-muted-foreground">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- External APIs -->
|
||||
<div class="space-y-4">
|
||||
<div class="bg-card rounded-lg border-2 border-amber-500/20 p-6">
|
||||
<h3 class="text-lg font-semibold mb-6 text-center text-amber-600 dark:text-amber-500">External APIs</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-amber-500/10 rounded-lg p-4 text-center">
|
||||
<div class="text-2xl mb-2">🐙</div>
|
||||
<h4 class="font-semibold">GitHub API</h4>
|
||||
<p class="text-sm text-muted-foreground mt-1">Repository Data Source</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-500/10 rounded-lg p-4 text-center">
|
||||
<div class="text-2xl mb-2">🍵</div>
|
||||
<h4 class="font-semibold">Gitea API</h4>
|
||||
<p class="text-sm text-muted-foreground mt-1">Mirror Destination</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-4 mt-6">
|
||||
<div class="text-center">
|
||||
<div class="text-sm text-muted-foreground mb-2">Data Flow</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-primary font-semibold">Backend</span>
|
||||
<svg class="w-6 h-6 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/>
|
||||
</svg>
|
||||
<span class="text-amber-600 dark:text-amber-500 font-semibold">APIs</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Component Breakdown -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Component Breakdown</h2>
|
||||
|
||||
<!-- Frontend -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Frontend (Astro + React)</h3>
|
||||
<div class="pl-4 border-l-2 border-primary/20">
|
||||
<p class="text-muted-foreground mb-4">
|
||||
The frontend is built with Astro, a modern web framework that allows for server-side rendering and partial hydration. React components are used for interactive elements, providing a responsive and dynamic user interface.
|
||||
</p>
|
||||
|
||||
<h4 class="font-semibold mb-3">Key Frontend Components</h4>
|
||||
<div class="space-y-3">
|
||||
{[
|
||||
{ name: 'Dashboard', desc: 'Overview of mirroring status and recent activity' },
|
||||
{ name: 'Repository Management', desc: 'Interface for managing repositories to mirror' },
|
||||
{ name: 'Organization Management', desc: 'Interface for managing GitHub organizations' },
|
||||
{ name: 'Configuration', desc: 'Settings for GitHub and Gitea connections' },
|
||||
{ name: 'Activity Log', desc: 'Detailed log of mirroring operations' }
|
||||
].map(component => (
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary font-mono text-sm">▸</span>
|
||||
<div>
|
||||
<strong>{component.name}</strong>
|
||||
<p class="text-sm text-muted-foreground mt-1">{component.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backend -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Backend (Bun)</h3>
|
||||
<div class="pl-4 border-l-2 border-primary/20">
|
||||
<p class="text-muted-foreground mb-4">
|
||||
The backend is built with Bun and provides API endpoints for the frontend to interact with. It handles:
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
{[
|
||||
'Authentication and user management',
|
||||
'GitHub API integration',
|
||||
'Gitea API integration',
|
||||
'Mirroring operations and job queue',
|
||||
'Real-time updates via Server-Sent Events (SSE) at /api/sse/',
|
||||
'Job recovery system for interrupted operations',
|
||||
'Graceful shutdown handling',
|
||||
'Scheduled automatic mirroring',
|
||||
'Database interactions with Drizzle ORM'
|
||||
].map(item => (
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary font-mono text-sm">▸</span>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database -->
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-4">Database (SQLite + Drizzle ORM)</h3>
|
||||
<div class="pl-4 border-l-2 border-primary/20">
|
||||
<p class="text-muted-foreground mb-4">
|
||||
SQLite with Bun's native SQLite driver is used for data persistence, with Drizzle ORM providing type-safe database interactions. The database stores:
|
||||
</p>
|
||||
|
||||
<div class="space-y-3">
|
||||
{[
|
||||
'User accounts and authentication data',
|
||||
'GitHub and Gitea configuration',
|
||||
'Repository and organization information',
|
||||
'Mirroring job history and status',
|
||||
'Event notifications and their read status'
|
||||
].map(item => (
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary font-mono text-sm">▸</span>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Data Flow -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Data Flow</h2>
|
||||
|
||||
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary">
|
||||
<ol class="space-y-4">
|
||||
{[
|
||||
{ title: 'User Authentication', desc: 'Users authenticate through the frontend, which communicates with the backend to validate credentials.' },
|
||||
{ title: 'Configuration', desc: 'Users configure GitHub and Gitea settings through the UI, which are stored in the SQLite database.' },
|
||||
{ title: 'Repository Discovery', desc: 'The backend queries the GitHub API to discover repositories based on user configuration.' },
|
||||
{ title: 'Mirroring Process', desc: 'When triggered, the backend fetches repository data from GitHub and pushes it to Gitea.' },
|
||||
{ title: 'Status Tracking', desc: 'All operations are logged in the database and displayed in the Activity Log.' }
|
||||
].map((step, index) => (
|
||||
<li class="flex gap-4">
|
||||
<span class="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div>
|
||||
<strong class="text-foreground">{step.title}</strong>
|
||||
<p class="text-sm text-muted-foreground mt-1">{step.desc}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Project Structure -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Project Structure</h2>
|
||||
|
||||
<div class="bg-muted/30 rounded-lg p-4">
|
||||
<pre class="text-sm"><code>{`gitea-mirror/
|
||||
├── src/ # Source code
|
||||
│ ├── components/ # React components
|
||||
│ ├── content/ # Documentation and content
|
||||
│ ├── layouts/ # Astro layout components
|
||||
│ ├── lib/ # Utility functions and database
|
||||
│ ├── pages/ # Astro pages and API routes
|
||||
│ └── styles/ # CSS and Tailwind styles
|
||||
├── public/ # Static assets
|
||||
├── data/ # Database and persistent data
|
||||
├── docker/ # Docker configuration
|
||||
└── scripts/ # Utility scripts for deployment and maintenance
|
||||
├── gitea-mirror-lxc-local.sh # Local LXC deployment script
|
||||
└── manage-db.ts # Database management tool`}</code></pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Deployment Options -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-6">Deployment Options</h2>
|
||||
|
||||
<p class="text-muted-foreground mb-6">Gitea Mirror supports multiple deployment options:</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
{[
|
||||
{ icon: '🐳', name: 'Docker', desc: 'Run as a containerized application using Docker and docker-compose' },
|
||||
{ icon: '📦', name: 'LXC Containers', desc: 'Deploy in Linux Containers on Proxmox VE using community script' },
|
||||
{ icon: '🏃', name: 'Native', desc: 'Run directly on the host system using Bun runtime' }
|
||||
].map(option => (
|
||||
<div class="bg-card rounded-lg border border-border p-4 hover:border-primary/50 transition-colors">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="text-2xl">{option.icon}</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-1">{option.name}</h4>
|
||||
<p class="text-sm text-muted-foreground">{option.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="bg-muted/30 rounded-lg p-4 mt-6">
|
||||
<h4 class="font-semibold mb-3">Deployment Advantages</h4>
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-3">
|
||||
<strong class="text-primary">Docker:</strong>
|
||||
<span class="text-muted-foreground">Isolation, easy updates, consistent environment</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<strong class="text-primary">LXC:</strong>
|
||||
<span class="text-muted-foreground">Lightweight virtualization, better performance than Docker, system-level isolation</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<strong class="text-primary">Native:</strong>
|
||||
<span class="text-muted-foreground">Best performance, direct access to system resources</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-muted-foreground">
|
||||
<p><strong>Note:</strong> LXC deployment is available through the <a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror" class="text-primary hover:underline">Proxmox VE Community Scripts</a> project.</p>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
512
src/pages/docs/configuration.astro
Normal file
512
src/pages/docs/configuration.astro
Normal file
@@ -0,0 +1,512 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
|
||||
const envVars = [
|
||||
{ name: 'NODE_ENV', desc: 'Runtime environment', default: 'development', example: 'production' },
|
||||
{ name: 'DATABASE_URL', desc: 'SQLite database URL', default: 'file:data/gitea-mirror.db', example: 'file:path/to/database.db' },
|
||||
{ name: 'JWT_SECRET', desc: 'Secret key for JWT auth', default: 'Auto-generated', example: 'your-secure-string' },
|
||||
{ name: 'HOST', desc: 'Server host', default: 'localhost', example: '0.0.0.0' },
|
||||
{ name: 'PORT', desc: 'Server port', default: '4321', example: '8080' }
|
||||
];
|
||||
|
||||
const githubOptions = [
|
||||
{ name: 'Username', desc: 'Your GitHub username', default: '-' },
|
||||
{ name: 'Token', desc: 'GitHub personal access token (Classic PAT)', default: '-' },
|
||||
{ name: 'Private Repositories', desc: 'Include private repositories', default: 'false' },
|
||||
{ name: 'Mirror Starred', desc: 'Mirror repositories you\'ve starred', default: 'false' },
|
||||
{ name: 'Mirror Issues', desc: 'Mirror issues from GitHub to Gitea', default: 'false' },
|
||||
{ name: 'Mirror Wiki', desc: 'Mirror wiki pages from GitHub to Gitea', default: 'false' },
|
||||
{ name: 'Mirror Organizations', desc: 'Mirror organization repositories', default: 'false' },
|
||||
{ name: 'Only Mirror Orgs', desc: 'Only mirror organization repositories', default: 'false' },
|
||||
{ name: 'Skip Forks', desc: 'Exclude repositories that are forks', default: 'false' },
|
||||
{ name: 'Skip Starred Issues', desc: 'Skip issues for starred repositories', default: 'false' }
|
||||
];
|
||||
|
||||
const giteaOptions = [
|
||||
{ name: 'URL', desc: 'Gitea server URL', default: '-' },
|
||||
{ name: 'Token', desc: 'Gitea access token', default: '-' },
|
||||
{ name: 'Organization', desc: 'Default organization for mirrored repositories', default: '-' },
|
||||
{ name: 'Visibility', desc: 'Default visibility for mirrored repositories', default: 'public' },
|
||||
{ name: 'Starred Repos Org', desc: 'Organization for starred repositories', default: 'github' }
|
||||
];
|
||||
---
|
||||
|
||||
<MainLayout title="Configuration - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<article class="bg-card rounded-2xl shadow-lg p-6 md:p-8 border border-border">
|
||||
<!-- Header -->
|
||||
<div class="mb-12 space-y-4">
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
<span>Configuration Guide</span>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Gitea Mirror Configuration</h1>
|
||||
<p class="text-lg text-muted-foreground leading-relaxed max-w-4xl">
|
||||
This guide provides detailed information on how to configure Gitea Mirror for your environment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Methods -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Configuration Methods</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||
<div class="bg-card rounded-lg border border-border p-6 hover:border-primary/50 transition-colors">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="text-2xl">🔧</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2">Environment Variables</h3>
|
||||
<p class="text-sm text-muted-foreground">Set configuration options through environment variables for automated deployments</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6 hover:border-primary/50 transition-colors">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="text-2xl">🖥️</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-2">Web UI</h3>
|
||||
<p class="text-sm text-muted-foreground">Configure the application through the web interface after installation</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Environment Variables -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Environment Variables</h2>
|
||||
|
||||
<p class="text-muted-foreground mb-6">The following environment variables can be used to configure Gitea Mirror:</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-border">
|
||||
<th class="text-left py-3 px-4 font-semibold">Variable</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{envVars.map((v, i) => (
|
||||
<tr class={`border-b border-border/50 hover:bg-muted/30 ${i === envVars.length - 1 ? 'border-b-0' : ''}`}>
|
||||
<td class="py-3 px-4">
|
||||
<code class="text-sm bg-muted px-1.5 py-0.5 rounded">{v.name}</code>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-muted-foreground">{v.desc}</td>
|
||||
<td class="py-3 px-4 text-sm"><code>{v.default}</code></td>
|
||||
<td class="py-3 px-4 text-sm"><code>{v.example}</code></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Security Note -->
|
||||
<div class="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4 mt-6">
|
||||
<div class="flex gap-3">
|
||||
<div class="text-amber-600 dark:text-amber-500">
|
||||
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-amber-600 dark:text-amber-500 mb-1">Security Note</h4>
|
||||
<p class="text-sm">The application will automatically generate a secure random <code class="bg-amber-500/10 px-1 py-0.5 rounded">JWT_SECRET</code> on first run if one isn't provided. This generated secret is stored in the data directory for persistence across container restarts.</p>
|
||||
<p class="text-sm mt-2">While this auto-generation feature provides good security by default, you can still explicitly set your own <code class="bg-amber-500/10 px-1 py-0.5 rounded">JWT_SECRET</code> for complete control over your deployment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Web UI Configuration -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Web UI Configuration</h2>
|
||||
|
||||
<p class="text-muted-foreground mb-6">After installing and starting Gitea Mirror, you can configure it through the web interface:</p>
|
||||
|
||||
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary mb-8">
|
||||
<ol class="space-y-3">
|
||||
{[
|
||||
'Navigate to <code class="bg-muted px-1.5 py-0.5 rounded text-sm">http://your-server:port/</code>',
|
||||
'If this is your first time, you\'ll be guided through creating an admin account',
|
||||
'Log in with your credentials',
|
||||
'Go to the Configuration page'
|
||||
].map((step, i) => (
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">{i + 1}</span>
|
||||
<span set:html={step}></span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- GitHub Configuration -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">GitHub Configuration</h3>
|
||||
<p class="text-muted-foreground mb-4">The GitHub configuration section allows you to connect to GitHub and specify which repositories to mirror.</p>
|
||||
|
||||
<div class="overflow-x-auto mb-6">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-border">
|
||||
<th class="text-left py-3 px-4 font-semibold">Option</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{githubOptions.map((opt, i) => (
|
||||
<tr class={`border-b border-border/50 hover:bg-muted/30 ${i === githubOptions.length - 1 ? 'border-b-0' : ''}`}>
|
||||
<td class="py-3 px-4 font-medium">{opt.name}</td>
|
||||
<td class="py-3 px-4 text-sm text-muted-foreground">{opt.desc}</td>
|
||||
<td class="py-3 px-4 text-sm"><code>{opt.default}</code></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- GitHub Token Permissions -->
|
||||
<div class="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4 mb-6">
|
||||
<div class="flex gap-3">
|
||||
<div class="text-blue-600 dark:text-blue-500">
|
||||
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-blue-600 dark:text-blue-500 mb-2">Required Permissions</h4>
|
||||
<p class="text-sm mb-3">You need to create a <span class="font-semibold">Classic GitHub PAT Token</span> with the following scopes:</p>
|
||||
<ul class="space-y-1 text-sm">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-blue-600 dark:text-blue-500">•</span>
|
||||
<span><code class="bg-blue-500/10 px-1 py-0.5 rounded">repo</code> - Full control of private repositories</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-blue-600 dark:text-blue-500">•</span>
|
||||
<span><code class="bg-blue-500/10 px-1 py-0.5 rounded">admin:org</code> - Full control of orgs and teams, read and write org projects</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-sm mt-2">The organization access is required for mirroring organization repositories.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-4 border-l-2 border-primary/20">
|
||||
<h5 class="font-semibold mb-3">To create a GitHub token:</h5>
|
||||
<ol class="space-y-2 text-sm">
|
||||
<li>Go to <a href="https://github.com/settings/tokens" class="text-primary hover:underline">GitHub Settings > Developer settings > Personal access tokens</a></li>
|
||||
<li>Click "Generate new token"</li>
|
||||
<li>Select the required permissions</li>
|
||||
<li>Copy the generated token and paste it into Gitea Mirror</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gitea Configuration -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Gitea Configuration</h3>
|
||||
<p class="text-muted-foreground mb-4">The Gitea configuration section allows you to connect to your Gitea instance and specify how repositories should be mirrored.</p>
|
||||
|
||||
<div class="overflow-x-auto mb-6">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-border">
|
||||
<th class="text-left py-3 px-4 font-semibold">Option</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{giteaOptions.map((opt, i) => (
|
||||
<tr class={`border-b border-border/50 hover:bg-muted/30 ${i === giteaOptions.length - 1 ? 'border-b-0' : ''}`}>
|
||||
<td class="py-3 px-4 font-medium">{opt.name}</td>
|
||||
<td class="py-3 px-4 text-sm text-muted-foreground">{opt.desc}</td>
|
||||
<td class="py-3 px-4 text-sm"><code>{opt.default}</code></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mirror Strategies -->
|
||||
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary mb-6">
|
||||
<h4 class="font-semibold text-lg mb-4">Mirror Strategies</h4>
|
||||
<p class="text-sm text-muted-foreground mb-4">Choose how your repositories will be organized in Gitea:</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-card rounded-lg border border-border p-4">
|
||||
<h5 class="font-semibold text-base mb-2 flex items-center gap-2">
|
||||
<span class="text-blue-600 dark:text-blue-500">📁</span>
|
||||
Preserve GitHub Structure
|
||||
</h5>
|
||||
<p class="text-sm text-muted-foreground mb-2">Maintains the exact structure from GitHub:</p>
|
||||
<ul class="space-y-1 text-sm ml-4">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span>Personal repos → Your Gitea username</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span>Organization repos → Same organization name in Gitea</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-4">
|
||||
<h5 class="font-semibold text-base mb-2 flex items-center gap-2">
|
||||
<span class="text-purple-600 dark:text-purple-500">🏢</span>
|
||||
Single Organization
|
||||
</h5>
|
||||
<p class="text-sm text-muted-foreground mb-2">Consolidates all repositories into one organization:</p>
|
||||
<ul class="space-y-1 text-sm ml-4">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span>All repos → One designated organization</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span>Requires setting "Organization" field</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-4">
|
||||
<h5 class="font-semibold text-base mb-2 flex items-center gap-2">
|
||||
<span class="text-green-600 dark:text-green-500">👤</span>
|
||||
Flat User Structure
|
||||
</h5>
|
||||
<p class="text-sm text-muted-foreground mb-2">Mirrors all repositories under your user account:</p>
|
||||
<ul class="space-y-1 text-sm ml-4">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span>All repos → Your Gitea username</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-muted-foreground">•</span>
|
||||
<span>No organizations needed</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-500/10 border border-amber-500/20 rounded-lg p-3 mt-4">
|
||||
<p class="text-sm">
|
||||
<span class="font-semibold">Note:</span> Starred repositories are always mirrored to the "Starred Repos Org" (default: "starred") regardless of the chosen strategy.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-4 border-l-2 border-primary/20">
|
||||
<h5 class="font-semibold mb-3">To create a Gitea access token:</h5>
|
||||
<ol class="space-y-2 text-sm">
|
||||
<li>Log in to your Gitea instance</li>
|
||||
<li>Go to Settings > Applications</li>
|
||||
<li>Under "Generate New Token", enter a name for your token</li>
|
||||
<li>Click "Generate Token"</li>
|
||||
<li>Copy the generated token and paste it into Gitea Mirror</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Configuration -->
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-4">Schedule Configuration</h3>
|
||||
<p class="text-muted-foreground mb-4">You can configure automatic mirroring on a schedule:</p>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-border">
|
||||
<th class="text-left py-3 px-4 font-semibold">Option</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Description</th>
|
||||
<th class="text-left py-3 px-4 font-semibold">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b border-border/50 hover:bg-muted/30">
|
||||
<td class="py-3 px-4 font-medium">Enable Scheduling</td>
|
||||
<td class="py-3 px-4 text-sm text-muted-foreground">Enable automatic mirroring</td>
|
||||
<td class="py-3 px-4 text-sm"><code>false</code></td>
|
||||
</tr>
|
||||
<tr class="hover:bg-muted/30">
|
||||
<td class="py-3 px-4 font-medium">Interval (seconds)</td>
|
||||
<td class="py-3 px-4 text-sm text-muted-foreground">Time between mirroring operations</td>
|
||||
<td class="py-3 px-4 text-sm"><code>3600</code> (1 hour)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Advanced Configuration -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Advanced Configuration</h2>
|
||||
|
||||
|
||||
<!-- Database Management -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Database Management</h3>
|
||||
<p class="text-muted-foreground mb-4">Gitea Mirror includes several database management tools that can be run from the command line:</p>
|
||||
|
||||
<div class="bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||
<pre class="text-sm whitespace-pre-wrap break-all"><code>{`# Initialize the database (only if it doesn't exist)
|
||||
bun run init-db
|
||||
|
||||
# Check database status
|
||||
bun run check-db
|
||||
|
||||
# Fix database location issues
|
||||
bun run fix-db
|
||||
|
||||
# Reset all users (for testing signup flow)
|
||||
bun run reset-users
|
||||
|
||||
# Remove database files completely
|
||||
bun run cleanup-db`}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Management -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Event Management</h3>
|
||||
<p class="text-muted-foreground mb-4">Events in Gitea Mirror (such as repository mirroring operations) are stored in the SQLite database and can be viewed in the Activity Log page.</p>
|
||||
|
||||
<div class="bg-green-500/10 border border-green-500/20 rounded-lg p-4">
|
||||
<div class="flex gap-3">
|
||||
<div class="text-green-600 dark:text-green-500">
|
||||
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm mb-2">Event Management Features:</p>
|
||||
<ul class="space-y-1 text-sm">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-green-600 dark:text-green-500">•</span>
|
||||
<span>View all events with filtering by type, status, and search</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-green-600 dark:text-green-500">•</span>
|
||||
<span>Real-time updates via Server-Sent Events (SSE)</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-green-600 dark:text-green-500">•</span>
|
||||
<span>Clean up old events using the cleanup button in the Activity Log</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-green-600 dark:text-green-500">•</span>
|
||||
<span>Automatic cleanup with configurable retention period</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Automatic Recovery System -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4">Automatic Recovery System</h3>
|
||||
<p class="text-muted-foreground mb-4">Gitea Mirror includes a robust recovery system that automatically handles interrupted operations:</p>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6">
|
||||
<ul class="space-y-3">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<div>
|
||||
<span class="font-semibold">Startup Recovery:</span>
|
||||
<p class="text-sm text-muted-foreground mt-1">Automatically recovers interrupted jobs when the application starts</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<div>
|
||||
<span class="font-semibold">Graceful Shutdown:</span>
|
||||
<p class="text-sm text-muted-foreground mt-1">Saves job state before shutting down to enable recovery on restart</p>
|
||||
</div>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<div>
|
||||
<span class="font-semibold">Job State Persistence:</span>
|
||||
<p class="text-sm text-muted-foreground mt-1">Stores mirror job progress in the database for resilience</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-4 bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||
<p class="text-sm font-medium mb-2">Manual recovery tools:</p>
|
||||
<pre class="text-sm whitespace-pre-wrap break-all"><code>{`# Run startup recovery manually
|
||||
bun run startup-recovery
|
||||
|
||||
# Fix interrupted jobs
|
||||
bun scripts/fix-interrupted-jobs.ts
|
||||
|
||||
# Test recovery system
|
||||
bun run test-recovery`}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Check Endpoint -->
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-4">Health Check Endpoint</h3>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6">
|
||||
<h4 class="font-semibold mb-3">System Health Monitoring</h4>
|
||||
<p class="text-muted-foreground mb-4">Gitea Mirror includes a built-in health check endpoint at <code class="bg-muted px-1.5 py-0.5 rounded">/api/health</code> that provides:</p>
|
||||
|
||||
<ul class="space-y-2 mb-6">
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<span>System status and uptime</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<span>Database connectivity check</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<span>Memory usage statistics</span>
|
||||
</li>
|
||||
<li class="flex gap-2">
|
||||
<span class="text-primary">✓</span>
|
||||
<span>Environment information</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||
<pre class="text-sm whitespace-pre-wrap break-all"><code>{`# Basic check (returns 200 OK if healthy)
|
||||
curl -I http://your-server:port/api/health
|
||||
|
||||
# Detailed health information (JSON)
|
||||
curl http://your-server:port/api/health`}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
@@ -1,18 +1,37 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { LuSettings, LuRocket, LuBookOpen } from 'react-icons/lu';
|
||||
|
||||
// Helper to pick an icon based on doc.slug
|
||||
// We'll use inline conditional rendering instead of this function
|
||||
// Define our documentation pages directly
|
||||
const docs = [
|
||||
{
|
||||
slug: 'architecture',
|
||||
title: 'Architecture',
|
||||
description: 'Comprehensive overview of the Gitea Mirror application architecture.',
|
||||
order: 1,
|
||||
icon: LuBookOpen,
|
||||
href: '/docs/architecture'
|
||||
},
|
||||
{
|
||||
slug: 'configuration',
|
||||
title: 'Configuration',
|
||||
description: 'Guide to configuring Gitea Mirror for your environment.',
|
||||
order: 2,
|
||||
icon: LuSettings,
|
||||
href: '/docs/configuration'
|
||||
},
|
||||
{
|
||||
slug: 'quickstart',
|
||||
title: 'Quick Start Guide',
|
||||
description: 'Get started with Gitea Mirror quickly.',
|
||||
order: 3,
|
||||
icon: LuRocket,
|
||||
href: '/docs/quickstart'
|
||||
}
|
||||
];
|
||||
|
||||
// Get all documentation entries, sorted by order
|
||||
const docs = await getCollection('docs');
|
||||
const sortedDocs = docs.sort((a, b) => {
|
||||
const orderA = a.data.order || 999;
|
||||
const orderB = b.data.order || 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
// Sort by order
|
||||
const sortedDocs = docs.sort((a, b) => a.order - b.order);
|
||||
---
|
||||
|
||||
<MainLayout title="Documentation">
|
||||
@@ -21,24 +40,25 @@ const sortedDocs = docs.sort((a, b) => {
|
||||
<p class="mb-10 text-lg text-muted-foreground text-center">Browse guides and technical docs for Gitea Mirror.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{sortedDocs.map(doc => (
|
||||
<a
|
||||
href={`/docs/${doc.slug}`}
|
||||
class="group block p-7 border border-border rounded-2xl bg-card hover:bg-muted transition-colors shadow-lg focus:ring-2 focus:ring-ring outline-none"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 bg-muted rounded-full flex items-center justify-center text-muted-foreground">
|
||||
{doc.slug === 'architecture' && <LuBookOpen className="w-5 h-5" />}
|
||||
{doc.slug === 'configuration' && <LuSettings className="w-5 h-5" />}
|
||||
{doc.slug === 'quickstart' && <LuRocket className="w-5 h-5" />}
|
||||
{!['architecture', 'configuration', 'quickstart'].includes(doc.slug) && <LuBookOpen className="w-5 h-5" />}
|
||||
{sortedDocs.map(doc => {
|
||||
const Icon = doc.icon;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={doc.href}
|
||||
class="group block p-7 border border-border rounded-2xl bg-card hover:bg-muted transition-colors shadow-lg focus:ring-2 focus:ring-ring outline-none"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="w-10 h-10 bg-muted rounded-full flex items-center justify-center text-muted-foreground">
|
||||
<Icon className="w-5 h-5" />
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold group-hover:text-foreground transition">{doc.title}</h2>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold group-hover:text-foreground transition">{doc.data.title}</h2>
|
||||
</div>
|
||||
<p class="text-muted-foreground">{doc.data.description}</p>
|
||||
</a>
|
||||
))}
|
||||
<p class="text-muted-foreground">{doc.description}</p>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
437
src/pages/docs/quickstart.astro
Normal file
437
src/pages/docs/quickstart.astro
Normal file
@@ -0,0 +1,437 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
---
|
||||
|
||||
<MainLayout title="Quick Start Guide - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<article class="bg-card rounded-2xl shadow-lg p-6 md:p-8 border border-border">
|
||||
<!-- Header -->
|
||||
<div class="mb-12 space-y-4">
|
||||
<div class="flex items-center gap-2 text-sm text-muted-foreground mb-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
<span>Quick Start</span>
|
||||
</div>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Gitea Mirror Quick Start Guide</h1>
|
||||
<p class="text-lg text-muted-foreground leading-relaxed max-w-4xl">
|
||||
This guide will help you get Gitea Mirror up and running quickly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Prerequisites -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Prerequisites</h2>
|
||||
|
||||
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary mb-8">
|
||||
<h3 class="font-semibold mb-4">Before you begin, make sure you have:</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">1</div>
|
||||
<div>
|
||||
<span class="font-semibold text-foreground">A GitHub account with a personal access token</span>
|
||||
<p class="text-sm text-muted-foreground mt-1">Create one at <a href="https://github.com/settings/tokens" class="text-primary hover:underline">GitHub Settings</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">2</div>
|
||||
<div>
|
||||
<span class="font-semibold text-foreground">A Gitea instance with an access token</span>
|
||||
<p class="text-sm text-muted-foreground mt-1">Available in your Gitea Settings > Applications</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">3</div>
|
||||
<div>
|
||||
<span class="font-semibold text-foreground">One of the following:</span>
|
||||
<ul class="mt-2 space-y-1 text-sm text-muted-foreground pl-4">
|
||||
<li class="flex gap-2"><span>•</span> Docker and docker-compose (for Docker deployment)</li>
|
||||
<li class="flex gap-2"><span>•</span> Bun 1.2.9+ (for native deployment)</li>
|
||||
<li class="flex gap-2"><span>•</span> Proxmox VE or LXD (for LXC container deployment)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Installation Options -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Installation Options</h2>
|
||||
<p class="text-muted-foreground mb-8">Choose the installation method that works best for your environment.</p>
|
||||
|
||||
<!-- Docker Installation -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
🐳 Using Docker <span class="text-sm text-muted-foreground font-normal">(Recommended for most users)</span>
|
||||
</h3>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6">
|
||||
<p class="text-muted-foreground mb-4">Docker provides the easiest way to get started with minimal configuration.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
{[
|
||||
{ step: 'Clone the repository', cmd: 'git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror' },
|
||||
{ step: 'Start the application in production mode', cmd: 'docker compose up -d' },
|
||||
{ step: 'Access the application', cmd: null, text: 'Open your browser and navigate to <a href="http://localhost:4321" class="text-primary hover:underline font-medium">http://localhost:4321</a>' }
|
||||
].map((item, i) => (
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-semibold text-muted-foreground">STEP {i + 1}</span>
|
||||
<span class="text-sm text-muted-foreground">{item.step}</span>
|
||||
</div>
|
||||
{item.cmd ? (
|
||||
<div class="bg-muted/30 rounded-lg p-4">
|
||||
<pre class="text-sm"><code>{item.cmd}</code></pre>
|
||||
</div>
|
||||
) : (
|
||||
<p class="text-sm" set:html={item.text}></p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bun Installation -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
🏃 Using Bun <span class="text-sm text-muted-foreground font-normal">(Native Installation)</span>
|
||||
</h3>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6">
|
||||
<p class="text-muted-foreground mb-4">If you prefer to run the application directly on your system:</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-semibold text-muted-foreground">STEP 1</span>
|
||||
<span class="text-sm text-muted-foreground">Clone the repository</span>
|
||||
</div>
|
||||
<div class="bg-muted/30 rounded-lg p-4">
|
||||
<pre class="text-sm"><code>git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-semibold text-muted-foreground">STEP 2</span>
|
||||
<span class="text-sm text-muted-foreground">Run the quick setup script</span>
|
||||
</div>
|
||||
<div class="bg-muted/30 rounded-lg p-4">
|
||||
<pre class="text-sm"><code>bun run setup</code></pre>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mt-2">This installs dependencies and initializes the database.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-semibold text-muted-foreground">STEP 3</span>
|
||||
<span class="text-sm text-muted-foreground">Choose how to run the application</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-3">
|
||||
<div class="border border-border rounded-lg p-4">
|
||||
<h5 class="font-semibold mb-2">Development Mode</h5>
|
||||
<div class="bg-muted/30 rounded-lg p-3 text-sm">
|
||||
<pre><code>bun run dev</code></pre>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-2">For Bun-specific features, use: <code class="bg-muted px-1 rounded">bunx --bun astro dev</code></p>
|
||||
</div>
|
||||
|
||||
<div class="border border-border rounded-lg p-4">
|
||||
<h5 class="font-semibold mb-2">Production Mode</h5>
|
||||
<div class="bg-muted/30 rounded-lg p-3 text-sm">
|
||||
<pre><code>bun run build
|
||||
bun run start</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-sm font-semibold text-muted-foreground">STEP 4</span>
|
||||
<span class="text-sm text-muted-foreground">Access the application</span>
|
||||
</div>
|
||||
<p class="text-sm">Open your browser and navigate to <a href="http://localhost:4321" class="text-primary hover:underline font-medium">http://localhost:4321</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LXC Installation -->
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold mb-4 flex items-center gap-2">
|
||||
📦 Using LXC Containers <span class="text-sm text-muted-foreground font-normal">(Recommended for Proxmox VE)</span>
|
||||
</h3>
|
||||
|
||||
<div class="bg-card rounded-lg border border-border p-6">
|
||||
<div class="space-y-4">
|
||||
<h4 class="font-semibold flex items-center gap-2">
|
||||
<span>Proxmox VE Installation</span>
|
||||
<span class="text-xs text-muted-foreground font-normal">(Community Script)</span>
|
||||
</h4>
|
||||
|
||||
<p class="text-sm text-muted-foreground mb-4">Deploy Gitea Mirror on Proxmox VE using the community-maintained script:</p>
|
||||
|
||||
<div class="bg-muted/30 rounded-lg p-4 overflow-x-auto">
|
||||
<pre class="text-sm whitespace-pre-wrap break-all"><code>bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/gitea-mirror.sh)"</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-muted-foreground">
|
||||
<p class="font-medium mb-2">This script will:</p>
|
||||
<ul class="space-y-1 pl-4">
|
||||
<li class="flex gap-2"><span>•</span> Create a privileged Alpine Linux LXC container</li>
|
||||
<li class="flex gap-2"><span>•</span> Install Bun runtime and dependencies</li>
|
||||
<li class="flex gap-2"><span>•</span> Clone and build Gitea Mirror</li>
|
||||
<li class="flex gap-2"><span>•</span> Configure systemd service for automatic startup</li>
|
||||
<li class="flex gap-2"><span>•</span> Set up the application to run on port 4321</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<div class="text-blue-600 dark:text-blue-500">
|
||||
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold text-blue-600 dark:text-blue-500 mb-1">Note</p>
|
||||
<p>After installation, access Gitea Mirror at <code class="bg-blue-500/10 px-1 py-0.5 rounded">http://<container-ip>:4321</code></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 border-t border-border">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
For more information about the community script, visit the
|
||||
<a href="https://community-scripts.github.io/ProxmoxVE/scripts?id=gitea-mirror" class="text-primary hover:underline">Community Scripts documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Initial Configuration -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Initial Configuration</h2>
|
||||
<p class="text-muted-foreground mb-6">Follow these steps to configure Gitea Mirror for first use:</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{[
|
||||
{
|
||||
num: '1',
|
||||
title: 'Create Admin Account',
|
||||
items: [
|
||||
"You'll be prompted on first access",
|
||||
'Choose a secure username and password',
|
||||
'This will be your administrator account'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '2',
|
||||
title: 'Configure GitHub Connection',
|
||||
items: [
|
||||
'Navigate to the Configuration page',
|
||||
'Enter your GitHub credentials',
|
||||
'Select repositories to mirror',
|
||||
'Configure filtering options'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '3',
|
||||
title: 'Configure Gitea Connection',
|
||||
items: [
|
||||
'Enter your Gitea server URL',
|
||||
'Enter your Gitea access token',
|
||||
'Configure organization settings',
|
||||
'Set default visibility'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '4',
|
||||
title: 'Set Up Scheduling',
|
||||
items: [
|
||||
'Enable automatic mirroring',
|
||||
'Set the mirroring interval',
|
||||
'Save your configuration'
|
||||
]
|
||||
}
|
||||
].map(step => (
|
||||
<div class="bg-card rounded-lg border border-border p-4 hover:border-primary/50 transition-colors">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">{step.num}</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-1">{step.title}</h4>
|
||||
<ul class="text-sm text-muted-foreground space-y-1">
|
||||
{step.items.map(item => (
|
||||
<li>• {item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Performing Your First Mirror -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Performing Your First Mirror</h2>
|
||||
|
||||
<div class="bg-gradient-to-r from-primary/5 to-transparent rounded-lg p-6 border-l-4 border-primary">
|
||||
<h3 class="font-semibold mb-4">After completing the configuration, you can start mirroring repositories:</h3>
|
||||
|
||||
<ol class="space-y-3">
|
||||
{[
|
||||
'Click <strong>"Import GitHub Data"</strong> to fetch repositories from GitHub',
|
||||
'Go to the <strong>Repositories</strong> page to view your imported repositories',
|
||||
'Select the repositories you want to mirror',
|
||||
'Click <strong>"Mirror Selected"</strong> to start the mirroring process',
|
||||
'Monitor the progress on the <strong>Activity</strong> page',
|
||||
"You'll receive toast notifications about the success or failure of operations"
|
||||
].map((step, i) => (
|
||||
<li class="flex gap-3">
|
||||
<span class="flex-shrink-0 w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center text-sm font-semibold">{i + 1}</span>
|
||||
<span set:html={step}></span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Troubleshooting -->
|
||||
<section class="mb-12">
|
||||
<h2 class="text-2xl font-bold mb-6">Troubleshooting</h2>
|
||||
|
||||
<div class="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4">
|
||||
<div class="flex gap-3">
|
||||
<div class="text-amber-600 dark:text-amber-500">
|
||||
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<h4 class="font-semibold text-amber-600 dark:text-amber-500">If you encounter any issues:</h4>
|
||||
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-500">•</span>
|
||||
<span>Check the <strong>Activity Log</strong> for detailed error messages</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-500">•</span>
|
||||
<span>Verify your GitHub and Gitea tokens have the correct permissions</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-500">•</span>
|
||||
<span>Ensure your Gitea instance is accessible from the machine running Gitea Mirror</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
<h5 class="font-semibold">Check logs based on your deployment method:</h5>
|
||||
|
||||
<div class="bg-amber-500/5 rounded-lg p-3 space-y-2">
|
||||
<div class="grid grid-cols-1 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="font-medium">Docker:</span>
|
||||
<code class="bg-amber-500/10 px-1.5 py-0.5 rounded ml-2">docker logs gitea-mirror</code>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">Native:</span>
|
||||
<span class="text-muted-foreground ml-2">Check terminal output or system logs</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium">LXC:</span>
|
||||
<code class="bg-amber-500/10 px-1.5 py-0.5 rounded ml-2">systemctl status gitea-mirror</code>
|
||||
<span class="text-muted-foreground ml-1">or</span>
|
||||
<code class="bg-amber-500/10 px-1.5 py-0.5 rounded ml-1">journalctl -u gitea-mirror -f</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-500">•</span>
|
||||
<span>Use the health check endpoint: <code class="bg-amber-500/10 px-1.5 py-0.5 rounded">curl http://your-server:4321/api/health</code></span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-500">•</span>
|
||||
<span>For database issues: <code class="bg-amber-500/10 px-1.5 py-0.5 rounded">bun run check-db</code> or <code class="bg-amber-500/10 px-1.5 py-0.5 rounded">bun run fix-db</code></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="my-12 h-px bg-border/50"></div>
|
||||
|
||||
<!-- Next Steps -->
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-6">Next Steps</h2>
|
||||
|
||||
<div class="bg-gradient-to-br from-primary/5 via-transparent to-primary/5 rounded-lg p-6 border border-border">
|
||||
<h3 class="font-semibold mb-4">After your initial setup:</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="space-y-3">
|
||||
{[
|
||||
'Explore the dashboard for an overview of your mirroring status',
|
||||
'Set up automatic mirroring schedules for hands-off operation',
|
||||
'Configure organization mirroring for team repositories',
|
||||
'Use the cleanup button in Activity Log to manage old events'
|
||||
].map(item => (
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">✓</span>
|
||||
<span>{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">📖</span>
|
||||
<span>Check out the <a href="/docs/configuration" class="text-primary hover:underline font-medium">Configuration Guide</a> for advanced settings</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">🏗️</span>
|
||||
<span>Review the <a href="/docs/architecture" class="text-primary hover:underline font-medium">Architecture Documentation</a> to understand the system</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">📊</span>
|
||||
<span>For server deployments, set up monitoring using the health check endpoint</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Config as ConfigType } from "@/lib/db/schema";
|
||||
|
||||
export type GiteaOrgVisibility = "public" | "private" | "limited";
|
||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user";
|
||||
export type MirrorStrategy = "preserve" | "single-org" | "flat-user" | "mixed";
|
||||
|
||||
export interface GiteaConfig {
|
||||
url: string;
|
||||
@@ -12,6 +12,7 @@ export interface GiteaConfig {
|
||||
starredReposOrg: string;
|
||||
preserveOrgStructure: boolean;
|
||||
mirrorStrategy?: MirrorStrategy; // New field for the strategy
|
||||
personalReposOrg?: string; // Override destination for personal repos
|
||||
}
|
||||
|
||||
export interface ScheduleConfig {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Simple test to verify that our security fix is working correctly
|
||||
* This test simulates the original security vulnerability and confirms it's been fixed
|
||||
*/
|
||||
|
||||
import { createSecureErrorResponse } from './src/lib/utils.js';
|
||||
|
||||
console.log('🔒 Testing Security Fix for Information Exposure...\n');
|
||||
|
||||
// Test 1: Sensitive error should be sanitized
|
||||
console.log('Test 1: Sensitive error with file path');
|
||||
const sensitiveError = new Error('ENOENT: no such file or directory, open \'/etc/passwd\'');
|
||||
const response1 = createSecureErrorResponse(sensitiveError, 'test', 500);
|
||||
|
||||
// Parse the response to check what's exposed
|
||||
const responseText1 = await response1.text();
|
||||
const responseData1 = JSON.parse(responseText1);
|
||||
|
||||
console.log('Original error:', sensitiveError.message);
|
||||
console.log('Sanitized response:', responseData1.error);
|
||||
console.log('✅ Sensitive path information hidden:', !responseData1.error.includes('/etc/passwd'));
|
||||
console.log('');
|
||||
|
||||
// Test 2: Safe error should be exposed
|
||||
console.log('Test 2: Safe error that should be exposed');
|
||||
const safeError = new Error('Missing required field: userId');
|
||||
const response2 = createSecureErrorResponse(safeError, 'test', 400);
|
||||
|
||||
const responseText2 = await response2.text();
|
||||
const responseData2 = JSON.parse(responseText2);
|
||||
|
||||
console.log('Original error:', safeError.message);
|
||||
console.log('Response:', responseData2.error);
|
||||
console.log('✅ Safe error properly exposed:', responseData2.error === safeError.message);
|
||||
console.log('');
|
||||
|
||||
// Test 3: Database connection error should be sanitized
|
||||
console.log('Test 3: Database connection error');
|
||||
const dbError = new Error('Connection failed: sqlite3://localhost:5432/secret_db?password=admin123');
|
||||
const response3 = createSecureErrorResponse(dbError, 'test', 500);
|
||||
|
||||
const responseText3 = await response3.text();
|
||||
const responseData3 = JSON.parse(responseText3);
|
||||
|
||||
console.log('Original error:', dbError.message);
|
||||
console.log('Sanitized response:', responseData3.error);
|
||||
console.log('✅ Database credentials hidden:', !responseData3.error.includes('password=admin123'));
|
||||
console.log('');
|
||||
|
||||
// Test 4: Stack trace should not be exposed
|
||||
console.log('Test 4: Stack trace exposure check');
|
||||
const errorWithStack = new Error('Internal server error');
|
||||
errorWithStack.stack = 'Error: Internal server error\n at /home/user/secret/app.js:123:45';
|
||||
const response4 = createSecureErrorResponse(errorWithStack, 'test', 500);
|
||||
|
||||
const responseText4 = await response4.text();
|
||||
const responseData4 = JSON.parse(responseText4);
|
||||
|
||||
console.log('Response keys:', Object.keys(responseData4));
|
||||
console.log('✅ Stack trace not exposed:', !responseData4.hasOwnProperty('stack'));
|
||||
console.log('✅ File paths not exposed:', !responseData4.error.includes('/home/user/secret'));
|
||||
console.log('');
|
||||
|
||||
console.log('🎉 All security tests passed! The vulnerability has been successfully fixed.');
|
||||
console.log('');
|
||||
console.log('Summary of fixes:');
|
||||
console.log('- ✅ Error details are logged server-side for debugging');
|
||||
console.log('- ✅ Only safe, whitelisted error messages are sent to clients');
|
||||
console.log('- ✅ Sensitive information like file paths, credentials, and stack traces are hidden');
|
||||
console.log('- ✅ Generic error message is returned for unsafe errors');
|
||||
console.log('- ✅ Timestamp is included for correlation with server logs');
|
||||
Reference in New Issue
Block a user