From f01d3f77ed35a21882e0e1e97310a13b0a9dfd8b Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sun, 28 Jun 2026 15:31:31 +0300 Subject: [PATCH] plan with paypal, 100 mb limit --- git/git.v | 62 ++++++++++- github.v | 102 ++++++++++++++++++ gitly.v | 10 ++ pr.v | 28 ++++- repo/repo.v | 52 ++++++++- repo/repo_routes.v | 46 +++++++- static/assets/version | 2 +- static/css/gitly.scss | 202 ++++++++++++++++++++++++++++++++++- templates/layout/header.html | 4 + templates/new.html | 4 + translations/cn.tr | 93 ++++++++++++++++ translations/en.tr | 93 ++++++++++++++++ translations/es.tr | 93 ++++++++++++++++ translations/jp.tr | 93 ++++++++++++++++ translations/pt.tr | 93 ++++++++++++++++ translations/ru.tr | 93 ++++++++++++++++ 16 files changed, 1057 insertions(+), 13 deletions(-) diff --git a/git/git.v b/git/git.v index 06f6f88..8047a39 100644 --- a/git/git.v +++ b/git/git.v @@ -3,6 +3,9 @@ module git import os import time +pub const clone_size_limit_exit_code = 125 +pub const clone_size_limit_marker = '[gitly:clone-size-limit]' + pub struct Git {} pub fn Git.exec(args []string) os.Result { @@ -62,6 +65,10 @@ pub fn Git.clone(url string, path string) os.Result { // `progress_path` while the clone is running, so a separate process can // poll the file and show live progress to the user. pub fn Git.clone_with_progress(url string, path string, progress_path string) os.Result { + return Git.clone_with_progress_limit(url, path, progress_path, 0) +} + +pub fn Git.clone_with_progress_limit(url string, path string, progress_path string, max_bytes u64) os.Result { println('new clone (progress) url="${url}" path="${path}" progress="${progress_path}"') os.rm(progress_path) or {} mut p := os.new_process('git') @@ -81,12 +88,26 @@ pub fn Git.clone_with_progress(url string, path string, progress_path string) os } } mut collected := '' + mut stopped_for_size := false for p.is_alive() { chunk := p.stderr_read() if chunk.len > 0 { log.write_string(chunk) or {} log.flush() collected += chunk + if max_bytes > 0 && !stopped_for_size + && clone_progress_received_bytes(collected) >= max_bytes { + stopped_for_size = true + marker := '\n${clone_size_limit_marker}\n' + log.write_string(marker) or {} + log.flush() + collected += marker + p.signal_term() + time.sleep(500 * time.millisecond) + if p.is_alive() { + p.signal_kill() + } + } } // drain stdout so the pipe buffer never blocks the child _ := p.stdout_read() @@ -100,7 +121,7 @@ pub fn Git.clone_with_progress(url string, path string, progress_path string) os } log.close() p.wait() - exit_code := p.code + exit_code := if stopped_for_size { clone_size_limit_exit_code } else { p.code } p.close() return os.Result{ exit_code: exit_code @@ -108,6 +129,45 @@ pub fn Git.clone_with_progress(url string, path string, progress_path string) os } } +pub fn clone_progress_received_bytes(progress string) u64 { + mut max_bytes := u64(0) + for line in progress.replace('\r', '\n').split('\n') { + if !line.contains('Receiving objects:') { + continue + } + bytes := parse_clone_progress_size(line) or { continue } + if bytes > max_bytes { + max_bytes = bytes + } + } + return max_bytes +} + +fn parse_clone_progress_size(line string) ?u64 { + comma := line.index('),') or { return none } + mut size_part := line[comma + 2..].trim_space() + pipe := size_part.index('|') or { size_part.len } + size_part = size_part[..pipe].trim_space() + parts := size_part.fields() + if parts.len < 2 { + return none + } + value := parts[0].f64() + multiplier := match parts[1] { + 'B', 'byte', 'bytes' { 1.0 } + 'KiB' { 1024.0 } + 'MiB' { 1024.0 * 1024.0 } + 'GiB' { 1024.0 * 1024.0 * 1024.0 } + else { return none } + } + + return u64(value * multiplier) +} + +pub fn Git.fetch_ref(repo_dir string, remote string, refspec string) os.Result { + return Git.exec_in_dir(repo_dir, ['fetch', remote, refspec]) +} + pub fn Git.show_file_blob(repo_dir string, branch string, file_path string) !string { result := Git.exec_in_dir(repo_dir, ['--no-pager', 'show', '${branch}:${file_path}']) if result.exit_code != 0 { diff --git a/github.v b/github.v index ca7451f..671a18f 100644 --- a/github.v +++ b/github.v @@ -6,6 +6,7 @@ import veb import x.json2 as json import net.http import time +import git // import veb.auth as oauth import veb.oauth @@ -24,6 +25,11 @@ struct GitHubPullRequestRef { url string } +struct GitHubPullRequestBranch { + ref_name string @[json: 'ref'] + sha string +} + struct GitHubLabel { name string color string @@ -53,6 +59,17 @@ struct GitHubIssue { labels []GitHubLabel } +struct GitHubPullRequest { + number int + title string + body string + state string + created_at string + user GitHubIssueAuthor + head GitHubPullRequestBranch + base GitHubPullRequestBranch +} + fn parse_github_timestamp(s string) int { if s == '' { return int(time.now().unix()) @@ -177,6 +194,91 @@ fn (mut app App) import_github_contributors(repo_id int, clone_url string) ! { eprintln('[github-contrib] done: imported ${total} contributors into repo ${repo_id}') } +fn (mut app App) import_github_pull_requests(repo Repo, owner_user_id int) ! { + eprintln('[github-pr] starting for repo_id=${repo.id} clone_url=${repo.clone_url} owner_user_id=${owner_user_id}') + owner, name := parse_github_owner_repo(repo.clone_url) or { + return error('cannot parse github url: ${repo.clone_url}') + } + defer { + app.sync_repo_open_pr_count(repo.id) or { + eprintln('[github-pr] cannot sync open PR count: ${err}') + } + } + mut page := 1 + mut imported := 0 + mut fetched := 0 + for page <= 100 { + url := 'https://api.github.com/repos/${owner}/${name}/pulls?state=open&per_page=100&page=${page}' + eprintln('[github-pr] GET ${url}') + mut req := http.new_request(.get, url, '') + req.add_header(.user_agent, 'gitly') + req.add_header(.accept, 'application/vnd.github+json') + resp := req.do() or { + eprintln('[github-pr] ERROR: request failed: ${err}') + return error('github api request failed: ${err}') + } + eprintln('[github-pr] page=${page} status=${resp.status_code} body_len=${resp.body.len}') + if resp.status_code != 200 { + eprintln('[github-pr] ERROR body: ${resp.body}') + return error('github api ${resp.status_code}: ${resp.body}') + } + prs := json.decode[[]GitHubPullRequest](resp.body) or { + eprintln('[github-pr] ERROR: cannot decode response: ${err}') + eprintln('[github-pr] response body was: ${resp.body#[..1000]}') + return error('cannot decode github pull requests: ${err}') + } + eprintln('[github-pr] decoded ${prs.len} pull requests on page ${page}') + if prs.len == 0 { + break + } + for gh_pr in prs { + if gh_pr.number <= 0 { + continue + } + head_branch := 'pr/${gh_pr.number}' + refspec := '+refs/pull/${gh_pr.number}/head:refs/heads/${head_branch}' + fetch_result := git.Git.fetch_ref(repo.git_dir, 'origin', refspec) + if fetch_result.exit_code != 0 { + eprintln('[github-pr] cannot fetch PR #${gh_pr.number}: ${fetch_result.output}') + continue + } + fetched++ + + base_branch := gh_pr.base.ref_name + if base_branch == '' || !is_safe_ref(base_branch) { + eprintln('[github-pr] skipping PR #${gh_pr.number}: invalid base branch "${base_branch}"') + continue + } + if app.pull_request_exists_for_head(repo.id, head_branch) { + continue + } + mut author_id := owner_user_id + if gh_pr.user.login != '' { + author_id = app.find_or_create_github_shadow_user(gh_pr.user.login) or { + eprintln('[github-pr] cannot resolve author @${gh_pr.user.login}: ${err}') + owner_user_id + } + } + created_at := parse_github_timestamp(gh_pr.created_at) + title := if gh_pr.title != '' { gh_pr.title } else { 'Pull request #${gh_pr.number}' } + app.add_imported_pull_request(repo.id, author_id, title, gh_pr.body, head_branch, + base_branch, created_at) or { + eprintln('[github-pr] ERROR inserting PR #${gh_pr.number}: ${err}') + continue + } + app.increment_repo_open_prs(repo.id) or { + eprintln('[github-pr] cannot bump PR count: ${err}') + } + imported++ + } + if prs.len < 100 { + break + } + page++ + } + eprintln('[github-pr] done: fetched ${fetched} PR refs, imported ${imported} pull requests into repo ${repo.id}') +} + // find_or_create_github_shadow_contributor is like find_or_create_github_shadow_user // but also stores the GitHub avatar URL when given. fn (mut app App) find_or_create_github_shadow_contributor(github_login string, avatar_url string) !int { diff --git a/gitly.v b/gitly.v index bc478d2..8e8144e 100644 --- a/gitly.v +++ b/gitly.v @@ -184,6 +184,16 @@ pub fn (mut app App) open_source() veb.Result { return $veb.html() } +@['/pricing'] +pub fn (mut app App) pricing() veb.Result { + return $veb.html('templates/pricing.html') +} + +@['/prcing'] +pub fn (mut app App) prcing() veb.Result { + return $veb.html('templates/pricing.html') +} + @['/'] pub fn (mut app App) index(mut ctx Context) veb.Result { user_count := app.get_users_count_with_reconnect() or { return ctx.db_error(err) } diff --git a/pr.v b/pr.v index e72ea0b..8e6dbb1 100644 --- a/pr.v +++ b/pr.v @@ -136,7 +136,7 @@ fn (rc &PrReviewComment) relative() string { return time.unix(rc.created_at).relative() } -fn (mut app App) add_pull_request(repo_id int, author_id int, title string, description string, head string, base string) !int { +fn (mut app App) add_pull_request_with_created_at(repo_id int, author_id int, title string, description string, head string, base string, created_at int) !int { pr := PullRequest{ repo_id: repo_id author_id: author_id @@ -145,7 +145,7 @@ fn (mut app App) add_pull_request(repo_id int, author_id int, title string, desc head_branch: head base_branch: base status: int(PrStatus.open) - created_at: int(time.now().unix()) + created_at: created_at } sql app.db { insert pr into PullRequest @@ -153,6 +153,23 @@ fn (mut app App) add_pull_request(repo_id int, author_id int, title string, desc return db_last_insert_id(mut app.db) } +fn (mut app App) add_pull_request(repo_id int, author_id int, title string, description string, head string, base string) !int { + return app.add_pull_request_with_created_at(repo_id, author_id, title, description, head, base, + int(time.now().unix())) +} + +fn (mut app App) add_imported_pull_request(repo_id int, author_id int, title string, description string, head string, base string, created_at int) !int { + return app.add_pull_request_with_created_at(repo_id, author_id, title, description, head, base, + created_at) +} + +fn (mut app App) pull_request_exists_for_head(repo_id int, head string) bool { + rows := sql app.db { + select count from PullRequest where repo_id == repo_id && head_branch == head + } or { 0 } + return rows > 0 +} + fn (mut app App) find_pull_request_by_id(pr_id int) ?PullRequest { rows := sql app.db { select from PullRequest where id == pr_id limit 1 @@ -183,6 +200,13 @@ fn (mut app App) get_repo_open_pr_count(repo_id int) int { } or { 0 } } +fn (mut app App) sync_repo_open_pr_count(repo_id int) ! { + open_prs_count := app.get_repo_open_pr_count(repo_id) + sql app.db { + update Repo set nr_open_prs = open_prs_count where id == repo_id + }! +} + fn (mut app App) set_pr_status(pr_id int, new_status PrStatus) ! { wanted := int(new_status) sql app.db { diff --git a/repo/repo.v b/repo/repo.v index e3aae2d..00f61cb 100644 --- a/repo/repo.v +++ b/repo/repo.v @@ -68,6 +68,7 @@ fn (r &Repo) wiki_enabled() bool { // log_field_separator is declared as constant in case we need to change it later const max_git_res_size = 1000 +const max_free_clone_size_bytes = u64(100) * 1024 * 1024 const log_field_separator = '\x7F' const ignored_folder = ['thirdparty'] @@ -401,6 +402,7 @@ fn (mut app App) update_repo_from_fs(mut repo Repo, recompute_lang_stats bool) ! repo.nr_contributors = app.get_count_repo_contributors(repo_id)! repo.nr_branches = app.get_count_repo_branches(repo_id) + repo.nr_open_prs = app.get_repo_open_pr_count(repo_id) // TODO: TEMPORARY - UNTIL WE GET PERSISTENT RELEASE INFO for tag in app.get_all_repo_tags(repo_id) { @@ -490,6 +492,7 @@ fn (mut app App) update_repo_from_remote(mut repo Repo) ! { repo.nr_contributors = app.get_count_repo_contributors(repo_id)! repo.nr_branches = app.get_count_repo_branches(repo_id) + repo.nr_open_prs = app.get_repo_open_pr_count(repo_id) app.save_repo(repo)! app.db.exec('END TRANSACTION')! @@ -1114,18 +1117,63 @@ fn (r &Repo) clone_progress_path() string { return r.git_dir + '.progress' } -fn (mut r Repo) clone() { +fn directory_size(path string) u64 { + if !os.exists(path) { + return 0 + } + if !os.is_dir(path) { + return os.file_size(path) + } + mut total := u64(0) + for entry in os.ls(path) or { return total } { + total += directory_size(os.join_path(path, entry)) + } + return total +} + +fn mark_clone_size_limit(progress_path string) { + mut log := os.open_append(progress_path) or { + os.write_file(progress_path, '${git.clone_size_limit_marker}\n') or {} + return + } + log.write_string('\n${git.clone_size_limit_marker}\n') or {} + log.close() +} + +fn cleanup_oversized_clone(repo_path string) { + os.rmdir_all(repo_path) or { eprintln('failed to remove oversized clone ${repo_path}: ${err}') } +} + +fn (mut r Repo) clone(enforce_size_limit bool) { eprintln('R CLONE') progress_path := r.clone_progress_path() - clone_result := git.Git.clone_with_progress(r.clone_url, r.git_dir, progress_path) + max_clone_size_bytes := if enforce_size_limit { max_free_clone_size_bytes } else { u64(0) } + clone_result := git.Git.clone_with_progress_limit(r.clone_url, r.git_dir, progress_path, + max_clone_size_bytes) clone_exit_code := clone_result.exit_code + if enforce_size_limit && clone_exit_code == git.clone_size_limit_exit_code { + r.status = .clone_failed + mark_clone_size_limit(progress_path) + cleanup_oversized_clone(r.git_dir) + println('git clone stopped because repo is larger than 100 MB') + return + } + if clone_exit_code != 0 { r.status = .clone_failed println('git clone failed with exit code ${clone_exit_code}') return } + if enforce_size_limit && directory_size(r.git_dir) >= max_free_clone_size_bytes { + r.status = .clone_failed + mark_clone_size_limit(progress_path) + cleanup_oversized_clone(r.git_dir) + println('git clone removed because repo is larger than 100 MB') + return + } + r.status = .done // progress file is no longer needed after a successful clone os.rm(progress_path) or {} diff --git a/repo/repo_routes.v b/repo/repo_routes.v index 6709c4c..b66169d 100644 --- a/repo/repo_routes.v +++ b/repo/repo_routes.v @@ -302,6 +302,8 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str is_public: is_public } import_issues := ctx.form['import_issues'] == '1' + import_prs := ctx.form['import_prs'] == '1' + eprintln('[new-repo] clone_url="${valid_clone_url}" import_issues=${import_issues} import_prs=${import_prs}') if is_clone_url_empty { os.mkdir(new_repo.git_dir) or { panic(err) } new_repo.git('init --bare') @@ -317,7 +319,8 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str if !is_clone_url_empty { app.debug('cloning') clone_job_repo := *new_repo - spawn clone_repo(clone_job_repo, app.config, import_issues, ctx.user.id) + spawn clone_repo(clone_job_repo, app.config, import_issues, import_prs, ctx.user.id, + !ctx.is_admin()) } new_repo2 := app.find_repo_by_name_and_username(new_repo.name, owner_name) or { app.info('Repo was not inserted') @@ -374,9 +377,9 @@ fn bg_fetch_files_info(repo_ Repo, branch string, path string, conf config.Confi app.db.close() or {} } -fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, owner_user_id int) { +fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, import_prs bool, owner_user_id int, enforce_clone_size_limit bool) { mut cloned_repo := new_repo - cloned_repo.clone() + cloned_repo.clone(enforce_clone_size_limit) // Use a dedicated DB connection for the clone thread to avoid // sharing a connection across threads. mut app := &App{ @@ -386,13 +389,27 @@ fn clone_repo(new_repo Repo, conf config.Config, import_issues bool, owner_user_ } config: conf } + if cloned_repo.status == .clone_failed { + app.set_repo_status(cloned_repo.id, .clone_failed) or { + eprintln('cannot set repo status ${err}') + } + app.db.close() or {} + return + } // Mark repo as done immediately so the user can browse it. // The tree page will fetch files from git on demand. app.set_repo_status(cloned_repo.id, .done) or { eprintln('cannot set repo status ${err}') } eprintln('clone done, repo available — indexing in background') - // For GitHub clones, also pull the repo description and contributors list - // (the issue import is gated on a separate user opt-in). + // For GitHub clones, also pull the repo description and contributors list. + // Issue and PR imports are gated on separate user opt-ins. Open PR refs are + // fetched before indexing so the branch scanner sees pr/ branches. if cloned_repo.clone_url.contains('github.com') { + eprintln('[clone] github imports repo_id=${cloned_repo.id} import_issues=${import_issues} import_prs=${import_prs}') + if import_prs { + app.import_github_pull_requests(cloned_repo, owner_user_id) or { + eprintln('[github-pr] FAILED: ${err}') + } + } spawn bg_import_github_repo_info(cloned_repo.id, cloned_repo.clone_url, cloned_repo.description, conf) if import_issues { @@ -473,6 +490,12 @@ fn read_clone_progress(progress_path string) string { if line == '' { continue } + if line == git.clone_size_limit_marker { + continue + } + if line.starts_with('Cloning into bare repository ') { + continue + } mut body := line if body.starts_with('remote: ') { body = body[8..] @@ -489,6 +512,11 @@ fn read_clone_progress(progress_path string) string { return stages.join('\n') } +fn clone_size_limit_failed(progress_path string) bool { + raw := os.read_file(progress_path) or { return false } + return raw.contains(git.clone_size_limit_marker) +} + @['/:username/:repo_name/tree/:branch_name/:path...'] pub fn (mut app App) tree(mut ctx Context, username string, repo_name string, branch_name string, path string) veb.Result { tree_t0 := time.ticks() @@ -501,6 +529,14 @@ pub fn (mut app App) tree(mut ctx Context, username string, repo_name string, br tree_t = time.ticks() mut clone_url := '' mut clone_progress := '' + if repo.status == .clone_failed { + clone_url = repo.clone_url + clone_progress = read_clone_progress(repo.clone_progress_path()) + if clone_size_limit_failed(repo.clone_progress_path()) { + return $veb.html('templates/clone_size_limit.html') + } + return ctx.not_found() + } if repo.status == .cloning { clone_url = repo.clone_url clone_progress = read_clone_progress(repo.clone_progress_path()) diff --git a/static/assets/version b/static/assets/version index 2cbd1d8..28cf3af 100644 --- a/static/assets/version +++ b/static/assets/version @@ -1 +1 @@ -4de5ced \ No newline at end of file +13ca859 \ No newline at end of file diff --git a/static/css/gitly.scss b/static/css/gitly.scss index 8143ae0..f730098 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -89,6 +89,64 @@ pre > code { } } +.clone-status--failed { + margin: 90px auto 0; + max-width: 700px; + padding: 0 14px; + + h1 { + font-size: 28px; + line-height: 1.2; + margin-bottom: 12px; + padding: 0; + } + + p { + color: $gray-dark; + font-size: 15px; + } +} + +.clone-status-button { + display: inline-block; + margin-top: 20px; + padding: 8px 14px; + border-radius: $small-radius; + background: #1f883d; + color: $white; + font-size: 14px; + font-weight: 600; + + &:hover { + background: #116329; + color: $white; + } +} + +.clone-status-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 20px; + + .clone-status-button { + margin-top: 0; + } +} + +.clone-status-button--secondary { + background: $white; + border: 1px solid #0969da; + color: #0969da; + + &:hover { + background: #0969da; + border-color: #0969da; + color: $white; + } +} + .clone-status-url { color: $gray-dark; margin: 10px auto 0; @@ -450,6 +508,144 @@ form { } } +.pricing-page { + margin-bottom: 90px; +} + +.pricing-hero { + padding: 48px 10px 26px; + text-align: center; + + h1 { + font-size: 34px; + line-height: 1.2; + margin-bottom: 10px; + padding: 0; + } + + p { + color: $gray-dark; + font-size: 16px; + margin: 0 auto; + max-width: 620px; + } +} + +.pricing-plans { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + padding: 0 10px; + + @include mobile { + grid-template-columns: 1fr; + } +} + +.pricing-plan { + border: 1px solid $gray; + border-radius: $medium-radius; + background: $white; + padding: 22px; + + h2 { + font-size: 24px; + line-height: 1.2; + padding: 0; + margin: 6px 0 8px; + } +} + +.pricing-plan--featured { + border-color: #1f883d; + box-shadow: 0 1px 5px rgba(31, 136, 61, 0.14); +} + +.pricing-plan__eyebrow { + color: #8250df; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + text-transform: uppercase; +} + +.pricing-plan__desc { + color: $gray-dark; + min-height: 44px; +} + +.pricing-plan__price { + display: flex; + align-items: baseline; + gap: 8px; + margin: 18px 0; + + span { + font-size: 32px; + font-weight: 700; + line-height: 1; + } + + small { + color: $gray-dark; + font-size: 14px; + } +} + +.pricing-plan__button { + display: block; + box-sizing: border-box; + width: 100%; + padding: 9px 12px; + border: 1px solid #1f883d; + border-radius: $small-radius; + background: #1f883d; + color: $white; + font-weight: 700; + text-align: center; + + &:hover { + background: #116329; + border-color: #116329; + color: $white; + } +} + +.pricing-plan__button--secondary { + background: #0969da; + border-color: #0969da; + + &:hover { + background: #0550ae; + border-color: #0550ae; + } +} + +.pricing-plan__features { + margin: 18px 0 0 18px; + + li { + margin-top: 8px; + } +} + +.pricing-note { + margin: 24px 10px 0; + padding-top: 18px; + border-top: 1px solid $gray; + + h2 { + font-size: 20px; + line-height: 1.2; + padding: 0; + margin-bottom: 8px; + } + + p { + color: $gray-dark; + } +} + @keyframes alert-show { 0% { right: -100px; } 100% { right: 20px; } @@ -881,7 +1077,10 @@ form { } .new-repo__field--inline { - gap: 0; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 12px 18px; } .new-repo__label { @@ -2450,4 +2649,3 @@ form { @import "files.scss", "langs.scss", "tree.scss", "commits.scss", "hl_table.scss", "branches.scss", "admin.scss", "blob.scss", "contributors.scss", "issues.scss", "user.scss", "releases.scss", "feed.scss", "search.scss"; - diff --git a/templates/layout/header.html b/templates/layout/header.html index ad93a39..b73b1b0 100644 --- a/templates/layout/header.html +++ b/templates/layout/header.html @@ -19,6 +19,10 @@ @end + +
@if ctx.logged_in .avatar { diff --git a/templates/new.html b/templates/new.html index 3d8985f..1ab1c2a 100644 --- a/templates/new.html +++ b/templates/new.html @@ -57,6 +57,10 @@ %new_repo_import_issues +
diff --git a/translations/cn.tr b/translations/cn.tr index 0547b05..117ca6a 100644 --- a/translations/cn.tr +++ b/translations/cn.tr @@ -215,6 +215,9 @@ new_repo_clone_hint new_repo_import_issues 从 GitHub 导入 issue ----- +new_repo_import_prs +从 GitHub 导入 PR +----- new_repo_visibility 仓库可见性 ----- @@ -842,3 +845,93 @@ admin_stats_new_issues admin_stats_last_n_days 最近 30 天 ----- +clone_size_limit_title +仓库超过 100 MB +----- +clone_size_limit_message +此仓库超过 100 MB。请购买每月仅 $5 的 Premium 订阅,以支持 Gitly,并帮助我们提供托管仓库所需的基础设施。 +----- +clone_size_limit_purchase_premium +用 PayPal 以 $5 购买 Premium +----- +clone_size_limit_pricing_link +查看 Premium 方案 +----- +header_pricing +价格 +----- +pricing_title +Gitly 价格 +----- +pricing_subtitle +选择符合仓库大小、存储需求和支持期望的方案。 +----- +pricing_best_for_individuals +适合个人 +----- +pricing_best_for_teams +适合团队 +----- +pricing_premium_name +Premium +----- +pricing_premium_desc +适合需要更大导入和私有项目托管的开发者。 +----- +pricing_premium_price +$5 +----- +pricing_per_month +每月 +----- +pricing_buy_premium +用 PayPal 购买 Premium +----- +pricing_premium_feature_one +克隆并托管最大 1 GB 的仓库 +----- +pricing_premium_feature_two +私有仓库和组织 +----- +pricing_premium_feature_three +导入 GitHub issues 和 pull requests +----- +pricing_premium_feature_four +10 GB 托管存储 +----- +pricing_premium_feature_five +标准支持队列 +----- +pricing_ultimate_name +Ultimate +----- +pricing_ultimate_desc +适合运行更大仓库和生产工作流的团队。 +----- +pricing_ultimate_price +$19 +----- +pricing_buy_ultimate +用 PayPal 购买 Ultimate +----- +pricing_ultimate_feature_one +包含 Premium 的全部功能 +----- +pricing_ultimate_feature_two +克隆并托管最大 10 GB 的仓库 +----- +pricing_ultimate_feature_three +100 GB 托管存储 +----- +pricing_ultimate_feature_four +高级权限和受保护工作流 +----- +pricing_ultimate_feature_five +基础设施问题优先支持 +----- +pricing_payment_title +付款 +----- +pricing_payment_note +付款通过 PayPal 发送至 +----- diff --git a/translations/en.tr b/translations/en.tr index 646f41a..6ba5588 100644 --- a/translations/en.tr +++ b/translations/en.tr @@ -218,6 +218,9 @@ new_repo_clone_hint new_repo_import_issues Import issues from GitHub ----- +new_repo_import_prs +Import PRs from GitHub +----- new_repo_visibility Repository visibility ----- @@ -1040,3 +1043,93 @@ Login via GitHub register_via_github Register via GitHub ----- +clone_size_limit_title +Repository is over 100 MB +----- +clone_size_limit_message +This repository is larger than 100 MB. Please purchase a Premium subscription for only $5/mo to support Gitly and help us provide the infrastructure needed for repository hosting. +----- +clone_size_limit_purchase_premium +Purchase Premium for $5 with PayPal +----- +clone_size_limit_pricing_link +View Premium plans +----- +header_pricing +Pricing +----- +pricing_title +Gitly pricing +----- +pricing_subtitle +Choose the plan that matches your repository size, storage needs, and support expectations. +----- +pricing_best_for_individuals +Best for individuals +----- +pricing_best_for_teams +Best for teams +----- +pricing_premium_name +Premium +----- +pricing_premium_desc +For developers who need larger imports and private project hosting. +----- +pricing_premium_price +$5 +----- +pricing_per_month +per month +----- +pricing_buy_premium +Buy Premium with PayPal +----- +pricing_premium_feature_one +Clone and host repositories up to 1 GB +----- +pricing_premium_feature_two +Private repositories and organizations +----- +pricing_premium_feature_three +GitHub issues and pull request import +----- +pricing_premium_feature_four +10 GB hosted storage +----- +pricing_premium_feature_five +Standard support queue +----- +pricing_ultimate_name +Ultimate +----- +pricing_ultimate_desc +For teams running heavier repositories and production workflows. +----- +pricing_ultimate_price +$19 +----- +pricing_buy_ultimate +Buy Ultimate with PayPal +----- +pricing_ultimate_feature_one +Everything in Premium +----- +pricing_ultimate_feature_two +Clone and host repositories up to 10 GB +----- +pricing_ultimate_feature_three +100 GB hosted storage +----- +pricing_ultimate_feature_four +Advanced permissions and protected workflows +----- +pricing_ultimate_feature_five +Priority support for infrastructure issues +----- +pricing_payment_title +Payment +----- +pricing_payment_note +Payments are handled via PayPal to +----- diff --git a/translations/es.tr b/translations/es.tr index 41dd918..e6c920c 100644 --- a/translations/es.tr +++ b/translations/es.tr @@ -215,6 +215,9 @@ new_repo_clone_hint new_repo_import_issues Importar issues desde GitHub ----- +new_repo_import_prs +Importar PRs desde GitHub +----- new_repo_visibility Visibilidad del repositorio ----- @@ -842,3 +845,93 @@ Nuevas incidencias admin_stats_last_n_days Últimos 30 días ----- +clone_size_limit_title +El repositorio supera los 100 MB +----- +clone_size_limit_message +Este repositorio supera los 100 MB. Compra una suscripción Premium por solo $5/mes para apoyar a Gitly y ayudarnos a ofrecer la infraestructura necesaria para alojar repositorios. +----- +clone_size_limit_purchase_premium +Comprar Premium por $5 con PayPal +----- +clone_size_limit_pricing_link +Ver planes Premium +----- +header_pricing +Precios +----- +pricing_title +Precios de Gitly +----- +pricing_subtitle +Elige el plan que encaja con el tamaño de tus repositorios, tus necesidades de almacenamiento y el soporte esperado. +----- +pricing_best_for_individuals +Ideal para individuos +----- +pricing_best_for_teams +Ideal para equipos +----- +pricing_premium_name +Premium +----- +pricing_premium_desc +Para desarrolladores que necesitan importaciones más grandes y alojamiento privado de proyectos. +----- +pricing_premium_price +$5 +----- +pricing_per_month +al mes +----- +pricing_buy_premium +Comprar Premium con PayPal +----- +pricing_premium_feature_one +Clona y aloja repositorios de hasta 1 GB +----- +pricing_premium_feature_two +Repositorios privados y organizaciones +----- +pricing_premium_feature_three +Importación de issues y pull requests de GitHub +----- +pricing_premium_feature_four +10 GB de almacenamiento alojado +----- +pricing_premium_feature_five +Cola de soporte estándar +----- +pricing_ultimate_name +Ultimate +----- +pricing_ultimate_desc +Para equipos con repositorios más pesados y flujos de producción. +----- +pricing_ultimate_price +$19 +----- +pricing_buy_ultimate +Comprar Ultimate con PayPal +----- +pricing_ultimate_feature_one +Todo lo incluido en Premium +----- +pricing_ultimate_feature_two +Clona y aloja repositorios de hasta 10 GB +----- +pricing_ultimate_feature_three +100 GB de almacenamiento alojado +----- +pricing_ultimate_feature_four +Permisos avanzados y flujos protegidos +----- +pricing_ultimate_feature_five +Soporte prioritario para problemas de infraestructura +----- +pricing_payment_title +Pago +----- +pricing_payment_note +Los pagos se gestionan con PayPal a +----- diff --git a/translations/jp.tr b/translations/jp.tr index 941888e..c43f734 100644 --- a/translations/jp.tr +++ b/translations/jp.tr @@ -215,6 +215,9 @@ new_repo_clone_hint new_repo_import_issues GitHubからissueをインポート ----- +new_repo_import_prs +GitHubからPRをインポート +----- new_repo_visibility リポジトリの公開設定 ----- @@ -842,3 +845,93 @@ admin_stats_new_issues admin_stats_last_n_days 直近30日 ----- +clone_size_limit_title +リポジトリが100 MBを超えています +----- +clone_size_limit_message +このリポジトリは100 MBを超えています。Gitlyを支援し、リポジトリホスティングに必要なインフラを提供できるよう、月額$5のPremiumをご購入ください。 +----- +clone_size_limit_purchase_premium +PayPalでPremiumを$5で購入 +----- +clone_size_limit_pricing_link +Premiumプランを見る +----- +header_pricing +料金 +----- +pricing_title +Gitlyの料金 +----- +pricing_subtitle +リポジトリサイズ、ストレージ、サポートの必要性に合うプランを選択してください。 +----- +pricing_best_for_individuals +個人向け +----- +pricing_best_for_teams +チーム向け +----- +pricing_premium_name +Premium +----- +pricing_premium_desc +より大きなインポートとプライベートプロジェクトのホスティングが必要な開発者向け。 +----- +pricing_premium_price +$5 +----- +pricing_per_month +月額 +----- +pricing_buy_premium +PayPalでPremiumを購入 +----- +pricing_premium_feature_one +最大1 GBのリポジトリをクローンしてホスト +----- +pricing_premium_feature_two +プライベートリポジトリと組織 +----- +pricing_premium_feature_three +GitHubのissuesとpull requestsをインポート +----- +pricing_premium_feature_four +10 GBのホストストレージ +----- +pricing_premium_feature_five +標準サポートキュー +----- +pricing_ultimate_name +Ultimate +----- +pricing_ultimate_desc +大きなリポジトリと本番ワークフローを扱うチーム向け。 +----- +pricing_ultimate_price +$19 +----- +pricing_buy_ultimate +PayPalでUltimateを購入 +----- +pricing_ultimate_feature_one +Premiumのすべて +----- +pricing_ultimate_feature_two +最大10 GBのリポジトリをクローンしてホスト +----- +pricing_ultimate_feature_three +100 GBのホストストレージ +----- +pricing_ultimate_feature_four +高度な権限と保護されたワークフロー +----- +pricing_ultimate_feature_five +インフラ問題への優先サポート +----- +pricing_payment_title +支払い +----- +pricing_payment_note +支払いはPayPalで次の宛先に送信されます: +----- diff --git a/translations/pt.tr b/translations/pt.tr index 420d9cd..9ad0acf 100644 --- a/translations/pt.tr +++ b/translations/pt.tr @@ -215,6 +215,9 @@ new_repo_clone_hint new_repo_import_issues Importar issues do GitHub ----- +new_repo_import_prs +Importar PRs do GitHub +----- new_repo_visibility Visibilidade do repositório ----- @@ -842,3 +845,93 @@ Novas issues admin_stats_last_n_days Últimos 30 dias ----- +clone_size_limit_title +O repositório tem mais de 100 MB +----- +clone_size_limit_message +Este repositório tem mais de 100 MB. Assine o Premium por apenas $5/mês para apoiar o Gitly e nos ajudar a oferecer a infraestrutura necessária para hospedar repositórios. +----- +clone_size_limit_purchase_premium +Comprar Premium por $5 com PayPal +----- +clone_size_limit_pricing_link +Ver planos Premium +----- +header_pricing +Preços +----- +pricing_title +Preços do Gitly +----- +pricing_subtitle +Escolha o plano que combina com o tamanho dos seus repositórios, suas necessidades de armazenamento e o suporte esperado. +----- +pricing_best_for_individuals +Ideal para indivíduos +----- +pricing_best_for_teams +Ideal para equipes +----- +pricing_premium_name +Premium +----- +pricing_premium_desc +Para desenvolvedores que precisam de importações maiores e hospedagem privada de projetos. +----- +pricing_premium_price +$5 +----- +pricing_per_month +por mês +----- +pricing_buy_premium +Comprar Premium com PayPal +----- +pricing_premium_feature_one +Clone e hospede repositórios de até 1 GB +----- +pricing_premium_feature_two +Repositórios privados e organizações +----- +pricing_premium_feature_three +Importação de issues e pull requests do GitHub +----- +pricing_premium_feature_four +10 GB de armazenamento hospedado +----- +pricing_premium_feature_five +Fila de suporte padrão +----- +pricing_ultimate_name +Ultimate +----- +pricing_ultimate_desc +Para equipes com repositórios maiores e fluxos de produção. +----- +pricing_ultimate_price +$19 +----- +pricing_buy_ultimate +Comprar Ultimate com PayPal +----- +pricing_ultimate_feature_one +Tudo do Premium +----- +pricing_ultimate_feature_two +Clone e hospede repositórios de até 10 GB +----- +pricing_ultimate_feature_three +100 GB de armazenamento hospedado +----- +pricing_ultimate_feature_four +Permissões avançadas e fluxos protegidos +----- +pricing_ultimate_feature_five +Suporte prioritário para problemas de infraestrutura +----- +pricing_payment_title +Pagamento +----- +pricing_payment_note +Os pagamentos são feitos via PayPal para +----- diff --git a/translations/ru.tr b/translations/ru.tr index 2fb8fab..86c0ae2 100644 --- a/translations/ru.tr +++ b/translations/ru.tr @@ -218,6 +218,9 @@ new_repo_clone_hint new_repo_import_issues Импортировать issues с GitHub ----- +new_repo_import_prs +Импортировать PR с GitHub +----- new_repo_visibility Видимость репозитория ----- @@ -1040,3 +1043,93 @@ login_via_github register_via_github Регистрация через GitHub ----- +clone_size_limit_title +Репозиторий больше 100 МБ +----- +clone_size_limit_message +Этот репозиторий больше 100 МБ. Пожалуйста, оформите Premium за $5/мес, чтобы поддержать Gitly и помочь нам оплачивать инфраструктуру для хостинга репозиториев. +----- +clone_size_limit_purchase_premium +Купить Premium за $5 через PayPal +----- +clone_size_limit_pricing_link +Посмотреть тарифы Premium +----- +header_pricing +Тарифы +----- +pricing_title +Тарифы Gitly +----- +pricing_subtitle +Выберите план под нужный размер репозиториев, объём хранилища и уровень поддержки. +----- +pricing_best_for_individuals +Лучше для разработчиков +----- +pricing_best_for_teams +Лучше для команд +----- +pricing_premium_name +Premium +----- +pricing_premium_desc +Для разработчиков, которым нужны большие импорты и приватный хостинг проектов. +----- +pricing_premium_price +$5 +----- +pricing_per_month +в месяц +----- +pricing_buy_premium +Купить Premium через PayPal +----- +pricing_premium_feature_one +Клонирование и хостинг репозиториев до 1 ГБ +----- +pricing_premium_feature_two +Приватные репозитории и организации +----- +pricing_premium_feature_three +Импорт issues и pull requests из GitHub +----- +pricing_premium_feature_four +10 ГБ хранилища +----- +pricing_premium_feature_five +Стандартная очередь поддержки +----- +pricing_ultimate_name +Ultimate +----- +pricing_ultimate_desc +Для команд с крупными репозиториями и производственными workflow. +----- +pricing_ultimate_price +$19 +----- +pricing_buy_ultimate +Купить Ultimate через PayPal +----- +pricing_ultimate_feature_one +Всё из Premium +----- +pricing_ultimate_feature_two +Клонирование и хостинг репозиториев до 10 ГБ +----- +pricing_ultimate_feature_three +100 ГБ хранилища +----- +pricing_ultimate_feature_four +Расширенные права доступа и защищённые workflow +----- +pricing_ultimate_feature_five +Приоритетная поддержка по вопросам инфраструктуры +----- +pricing_payment_title +Оплата +----- +pricing_payment_note +Платежи принимаются через PayPal на +----- -- 2.39.5