mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 22:23:50 +03:00
Merge pull request '[gitea] cherry-pick' (#2375) from earl-warren/forgejo:wip-gitea-cherry-pick into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/2375 Reviewed-by: Otto <otto@codeberg.org>
This commit is contained in:
commit
6c9c0aca76
67 changed files with 3542 additions and 880 deletions
|
@ -12,6 +12,7 @@ plugins:
|
|||
- "@eslint-community/eslint-plugin-eslint-comments"
|
||||
- "@stylistic/eslint-plugin-js"
|
||||
- eslint-plugin-array-func
|
||||
- eslint-plugin-github
|
||||
- eslint-plugin-i
|
||||
- eslint-plugin-jquery
|
||||
- eslint-plugin-no-jquery
|
||||
|
@ -209,6 +210,29 @@ rules:
|
|||
func-names: [0]
|
||||
func-style: [0]
|
||||
getter-return: [2]
|
||||
github/a11y-aria-label-is-well-formatted: [0]
|
||||
github/a11y-no-title-attribute: [0]
|
||||
github/a11y-no-visually-hidden-interactive-element: [0]
|
||||
github/a11y-role-supports-aria-props: [0]
|
||||
github/a11y-svg-has-accessible-name: [0]
|
||||
github/array-foreach: [0]
|
||||
github/async-currenttarget: [2]
|
||||
github/async-preventdefault: [2]
|
||||
github/authenticity-token: [0]
|
||||
github/get-attribute: [0]
|
||||
github/js-class-name: [0]
|
||||
github/no-blur: [0]
|
||||
github/no-d-none: [0]
|
||||
github/no-dataset: [2]
|
||||
github/no-dynamic-script-tag: [2]
|
||||
github/no-implicit-buggy-globals: [2]
|
||||
github/no-inner-html: [0]
|
||||
github/no-innerText: [2]
|
||||
github/no-then: [2]
|
||||
github/no-useless-passive: [2]
|
||||
github/prefer-observers: [2]
|
||||
github/require-passive-events: [2]
|
||||
github/unescaped-html-literal: [0]
|
||||
grouped-accessor-pairs: [2]
|
||||
guard-for-in: [0]
|
||||
id-blacklist: [0]
|
||||
|
|
5
Makefile
5
Makefile
|
@ -1023,3 +1023,8 @@ docker:
|
|||
|
||||
# This endif closes the if at the top of the file
|
||||
endif
|
||||
|
||||
# Disable parallel execution because it would break some targets that don't
|
||||
# specify exact dependencies like 'backend' which does currently not depend
|
||||
# on 'frontend' to enable Node.js-less builds from source tarballs.
|
||||
.NOTPARALLEL:
|
||||
|
|
|
@ -79,4 +79,8 @@ async function main() {
|
|||
]);
|
||||
}
|
||||
|
||||
main().then(exit).catch(exit);
|
||||
try {
|
||||
exit(await main());
|
||||
} catch (err) {
|
||||
exit(err);
|
||||
}
|
||||
|
|
|
@ -63,4 +63,8 @@ async function main() {
|
|||
]);
|
||||
}
|
||||
|
||||
main().then(exit).catch(exit);
|
||||
try {
|
||||
exit(await main());
|
||||
} catch (err) {
|
||||
exit(err);
|
||||
}
|
||||
|
|
|
@ -161,7 +161,11 @@ func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int6
|
|||
}
|
||||
|
||||
for _, item := range res {
|
||||
item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
|
||||
if item.UserID > 0 {
|
||||
item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
|
||||
} else {
|
||||
item.UserAvatarLink = avatars.DefaultAvatarLink()
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -382,6 +382,7 @@ email_not_associate=Η διεύθυνση ηλεκτρονικού ταχυδρ
|
|||
send_reset_mail=Αποστολή Email Ανάκτησης Λογαριασμού
|
||||
reset_password=Ανάκτηση Λογαριασμού
|
||||
invalid_code=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έχει λήξει.
|
||||
invalid_code_forgot_password=Ο κωδικός επιβεβαίωσης δεν είναι έγκυρος ή έληξε. Πατήστε <a href="%s">εδώ</a> για να ξεκινήσετε νέα συνεδρία.
|
||||
invalid_password=Ο κωδικός πρόσβασης σας δεν ταιριάζει με τον κωδικό που χρησιμοποιήθηκε για τη δημιουργία του λογαριασμού.
|
||||
reset_password_helper=Ανάκτηση Λογαριασμού
|
||||
reset_password_wrong_user=Έχετε συνδεθεί ως %s, αλλά ο σύνδεσμος ανάκτησης λογαριασμού προορίζεται για το %s
|
||||
|
@ -865,10 +866,12 @@ revoke_oauth2_grant_description=Η ανάκληση πρόσβασης για α
|
|||
revoke_oauth2_grant_success=Η πρόσβαση ανακλήθηκε επιτυχώς.
|
||||
|
||||
twofa_desc=Ο έλεγχος ταυτότητας δύο παραγόντων ενισχύει την ασφάλεια του λογαριασμού σας.
|
||||
twofa_recovery_tip=Αν χάσετε τη συσκευή σας, θα είστε σε θέση να χρησιμοποιήσετε ένα κλειδί ανάκτησης μιας χρήσης για να ανακτήσετε την πρόσβαση στο λογαριασμό σας.
|
||||
twofa_is_enrolled=Ο λογαριασμός σας είναι <strong>εγγεγραμμένος</strong> σε έλεγχο ταυτότητας δύο παραγόντων.
|
||||
twofa_not_enrolled=Ο λογαριασμός σας δεν είναι εγγεγραμμένος σε έλεγχο ταυτότητας δύο παραγόντων.
|
||||
twofa_disable=Απενεργοποίηση Ταυτοποίησης Δύο Παραμέτρων
|
||||
twofa_scratch_token_regenerate=Αναδημιουργία Διακριτικού Μίας Χρήσης
|
||||
twofa_scratch_token_regenerated=Το κλειδί ανάκτησης μιας χρήσης είναι τώρα %s. Αποθηκεύστε το σε ασφαλές μέρος, καθώς δε θα εμφανιστεί ξανά.
|
||||
twofa_enroll=Εγγραφή στην ταυτοποίηση δύο παραγόντων
|
||||
twofa_disable_note=Μπορείτε να απενεργοποιήσετε την ταυτοποίηση δύο παραγόντων αν χρειαστεί.
|
||||
twofa_disable_desc=Η απενεργοποίηση της ταυτοποίησης δύο παραγόντων θα καταστήσει τον λογαριασμό σας λιγότερο ασφαλή. Συνέχεια;
|
||||
|
@ -886,6 +889,8 @@ webauthn_register_key=Προσθήκη Κλειδιού Ασφαλείας
|
|||
webauthn_nickname=Ψευδώνυμο
|
||||
webauthn_delete_key=Αφαίρεση Κλειδιού Ασφαλείας
|
||||
webauthn_delete_key_desc=Αν αφαιρέσετε ένα κλειδί ασφαλείας δεν μπορείτε πλέον να συνδεθείτε με αυτό. Συνέχεια;
|
||||
webauthn_key_loss_warning=Αν χάσετε τα κλειδιά ασφαλείας σας, θα χάσετε την πρόσβαση στο λογαριασμό σας.
|
||||
webauthn_alternative_tip=Μπορεί να θέλετε να ρυθμίσετε μια πρόσθετη μέθοδο ταυτοποίησης.
|
||||
|
||||
manage_account_links=Διαχείριση Συνδεδεμένων Λογαριασμών
|
||||
manage_account_links_desc=Αυτοί οι εξωτερικοί λογαριασμοί είναι συνδεδεμένοι στον Forgejo λογαριασμό σας.
|
||||
|
@ -895,6 +900,7 @@ remove_account_link=Αφαίρεση Συνδεδεμένου Λογαριασμ
|
|||
remove_account_link_desc=Η κατάργηση ενός συνδεδεμένου λογαριασμού θα ανακαλέσει την πρόσβασή του στο λογαριασμό σας στο Forgejo. Συνέχεια;
|
||||
remove_account_link_success=Ο συνδεδεμένος λογαριασμός έχει αφαιρεθεί.
|
||||
|
||||
hooks.desc=Προσθήκη webhooks που θα ενεργοποιούνται για <strong>όλα τα αποθετήρια</strong> που σας ανήκουν.
|
||||
|
||||
orgs_none=Δεν είστε μέλος σε κάποιο οργανισμό.
|
||||
repos_none=Δεν κατέχετε κάποιο αποθετήριο.
|
||||
|
@ -916,9 +922,12 @@ visibility=Ορατότητα χρήστη
|
|||
visibility.public=Δημόσια
|
||||
visibility.public_tooltip=Ορατό σε όλους
|
||||
visibility.limited=Περιορισμένη
|
||||
visibility.limited_tooltip=Ορατό μόνο στους ταυτοποιημένους χρήστες
|
||||
visibility.private=Ιδιωτική
|
||||
visibility.private_tooltip=Ορατό μόνο στα μέλη των οργανισμών που συμμετέχετε
|
||||
|
||||
[repo]
|
||||
new_repo_helper=Ένα αποθετήριο περιέχει όλα τα αρχεία έργου, συμπεριλαμβανομένου του ιστορικού εκδόσεων. Ήδη φιλοξενείται αλλού; <a href="%s">Μετεγκατάσταση αποθετηρίου.</a>
|
||||
owner=Ιδιοκτήτης
|
||||
owner_helper=Ορισμένοι οργανισμοί ενδέχεται να μην εμφανίζονται στο αναπτυσσόμενο μενού λόγω του μέγιστου αριθμού αποθετηρίων.
|
||||
repo_name=Όνομα αποθετηρίου
|
||||
|
@ -941,6 +950,7 @@ fork_to_different_account=Fork σε διαφορετικό λογαριασμό
|
|||
fork_visibility_helper=Η ορατότητα ενός fork αποθετηρίου δεν μπορεί να αλλάξει.
|
||||
fork_branch=Κλάδος που θα κλωνοποιηθεί στο fork
|
||||
all_branches=Όλοι οι κλάδοι
|
||||
fork_no_valid_owners=Αυτό το αποθετήριο δεν μπορεί να γίνει fork επειδή δεν υπάρχουν έγκυροι ιδιοκτήτες.
|
||||
use_template=Χρήση αυτού του πρότυπου
|
||||
clone_in_vsc=Κλωνοποίηση στο VS Code
|
||||
download_zip=Λήψη ZIP
|
||||
|
@ -1005,13 +1015,20 @@ delete_preexisting=Διαγραφή αρχείων που προϋπήρχαν
|
|||
delete_preexisting_content=Διαγραφή αρχείων στο %s
|
||||
delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημένα αρχεία στο %s
|
||||
blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή
|
||||
blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>. Πατήστε <a href="%s">εδώ</a> για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών.
|
||||
blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο <a href="%s">.git-blame-ignore-revs</a>.
|
||||
author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών
|
||||
|
||||
tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s
|
||||
tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s
|
||||
tree_path_not_found_tag=Η διαδρομή %[1]s δεν υπάρχει στην ετικέτα %[2]s
|
||||
|
||||
transfer.accept=Αποδοχή Μεταφοράς
|
||||
transfer.accept_desc=`Μεταφορά στο "%s"`
|
||||
transfer.reject=Απόρριψη Μεταφοράς
|
||||
transfer.reject_desc=`Ακύρωση μεταφοράς σε "%s"`
|
||||
transfer.no_permission_to_accept=Δεν έχετε άδεια να αποδεχτείτε αυτή τη μεταφορά.
|
||||
transfer.no_permission_to_reject=Δεν έχετε άδεια να απορρίψετε αυτή τη μεταφορά.
|
||||
|
||||
desc.private=Ιδιωτικό
|
||||
desc.public=Δημόσιο
|
||||
|
@ -1030,6 +1047,8 @@ template.issue_labels=Σήματα Ζητήματος
|
|||
template.one_item=Πρέπει να επιλέξετε τουλάχιστον ένα αντικείμενο στο πρότυπο
|
||||
template.invalid=Πρέπει να επιλέξετε ένα πρότυπο αποθετήριο
|
||||
|
||||
archive.title=Αυτό το αποθετήρειο αρχειοθετήθηκε. Μπορείτε να προβάλετε αρχεία και να τα κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests.
|
||||
archive.title_date=Αυτό το αποθετήριο έχει αρχειοθετηθεί στο %s. Μπορείτε να προβάλετε αρχεία και να κλωνοποιήσετε, αλλά δεν μπορείτε να ωθήσετε ή να ανοίξετε ζητήματα ή pull requests.
|
||||
archive.issue.nocomment=Αυτό το αποθετήριο αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε σε ζητήματα.
|
||||
archive.pull.nocomment=Αυτό το repo αρχειοθετήθηκε. Δεν μπορείτε να σχολιάσετε στα pull requests.
|
||||
|
||||
|
@ -1046,6 +1065,7 @@ migrate_options_lfs=Μεταφορά αρχείων LFS
|
|||
migrate_options_lfs_endpoint.label=Άκρο LFS
|
||||
migrate_options_lfs_endpoint.description=Η μεταφορά θα προσπαθήσει να χρησιμοποιήσει το Git remote για να <a target="_blank" rel="noopener noreferrer" href="%s">καθορίσει τον διακομιστή LFS</a>. Μπορείτε επίσης να καθορίσετε ένα δικό σας endpoint αν τα δεδομένα LFS του αποθετηρίου αποθηκεύονται κάπου αλλού.
|
||||
migrate_options_lfs_endpoint.description.local=Μια διαδρομή στο τοπικό διακομιστή επίσης υποστηρίζεται.
|
||||
migrate_options_lfs_endpoint.placeholder=Αν αφεθεί κενό, το άκρο θα προκύψει από το URL του κλώνου
|
||||
migrate_items=Στοιχεία Μεταφοράς
|
||||
migrate_items_wiki=Wiki
|
||||
migrate_items_milestones=Ορόσημα
|
||||
|
@ -1148,6 +1168,7 @@ file_view_rendered=Προβολή Απόδοσης
|
|||
file_view_raw=Προβολή Ακατέργαστου
|
||||
file_permalink=Permalink
|
||||
file_too_large=Το αρχείο είναι πολύ μεγάλο για να εμφανιστεί.
|
||||
invisible_runes_header=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode `
|
||||
invisible_runes_description=`Αυτό το αρχείο περιέχει αόρατους χαρακτήρες Unicode που δεν διακρίνονται από ανθρώπους, αλλά μπορεί να επεξεργάζονται διαφορετικά από έναν υπολογιστή. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
|
||||
ambiguous_runes_header=`Αυτό το αρχείο περιέχει ασαφείς χαρακτήρες Unicode `
|
||||
ambiguous_runes_description=`Αυτό το αρχείο περιέχει χαρακτήρες Unicode που μπορεί να συγχέονται με άλλους χαρακτήρες. Αν νομίζετε ότι αυτό είναι σκόπιμο, μπορείτε να αγνοήσετε με ασφάλεια αυτή την προειδοποίηση. Χρησιμοποιήστε το κουμπί Escape για να τους αποκαλύψετε.`
|
||||
|
@ -1426,6 +1447,7 @@ issues.filter_sort.moststars=Περισσότερα αστέρια
|
|||
issues.filter_sort.feweststars=Λιγότερα αστέρια
|
||||
issues.filter_sort.mostforks=Περισσότερα forks
|
||||
issues.filter_sort.fewestforks=Λιγότερα forks
|
||||
issues.keyword_search_unavailable=Η αναζήτηση μέσω λέξεων κλειδιών δεν είναι διαθέσιμη. Παρακαλώ επικοινωνήστε με το διαχειριστή.
|
||||
issues.action_open=Άνοιγμα
|
||||
issues.action_close=Κλείσιμο
|
||||
issues.action_label=Σήμα
|
||||
|
@ -1716,8 +1738,12 @@ pulls.is_empty=Οι αλλαγές σε αυτόν τον κλάδο είναι
|
|||
pulls.required_status_check_failed=Ορισμένοι απαιτούμενοι έλεγχοι δεν ήταν επιτυχείς.
|
||||
pulls.required_status_check_missing=Λείπουν ορισμένοι απαιτούμενοι έλεγχοι.
|
||||
pulls.required_status_check_administrator=Ως διαχειριστής, μπορείτε ακόμα να συγχωνεύσετε αυτό το pull request.
|
||||
pulls.blocked_by_approvals=Το pull request δεν έχει ακόμα αρκετές εγκρίσεις. Δόθηκαν %d από %d εγκρίσεις.
|
||||
pulls.blocked_by_rejection=Αυτό το Pull Request έχει αλλαγές που ζητούνται από έναν επίσημο εξεταστή.
|
||||
pulls.blocked_by_official_review_requests=Αυτό το Pull Request έχει επίσημες αιτήσεις αξιολόγησης.
|
||||
pulls.blocked_by_outdated_branch=Αυτό το pull request έχει αποκλειστεί επειδή είναι παρωχημένο.
|
||||
pulls.blocked_by_changed_protected_files_1=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει ένα προστατευμένο αρχείο:
|
||||
pulls.blocked_by_changed_protected_files_n=Αυτό το pull request έχει αποκλειστεί επειδή αλλάζει προστατευμένα αρχεία:
|
||||
pulls.can_auto_merge_desc=Αυτό το Pull Request μπορεί να συγχωνευθεί αυτόματα.
|
||||
pulls.cannot_auto_merge_desc=Αυτό το pull request δεν μπορεί να συγχωνευθεί αυτόματα λόγω συγκρούσεων.
|
||||
pulls.cannot_auto_merge_helper=Χειροκίνητη Συγχώνευση για την επίλυση των συγκρούσεων.
|
||||
|
@ -1833,11 +1859,16 @@ milestones.filter_sort.least_issues=Λιγότερα ζητήματα
|
|||
|
||||
signing.will_sign=Αυτή η υποβολή θα υπογραφεί με το κλειδί "%s".
|
||||
signing.wont_sign.error=Παρουσιάστηκε σφάλμα κατά τον έλεγχο για το αν η υποβολή μπορεί να υπογραφεί.
|
||||
signing.wont_sign.nokey=Δεν υπάρχει διαθέσιμο κλειδί για να υπογραφεί αυτή η υποβολή.
|
||||
signing.wont_sign.never=Οι υποβολές δεν υπογράφονται ποτέ.
|
||||
signing.wont_sign.always=Οι υποβολές υπογράφονται πάντα.
|
||||
signing.wont_sign.pubkey=Η υποβολή δε θα υπογραφεί επειδή δεν υπάρχει δημόσιο κλειδί που να συνδέεται με το λογαριασμό σας.
|
||||
signing.wont_sign.twofa=Πρέπει να έχετε ενεργοποιημένη την ταυτοποίηση δύο παραγόντων για να υπογράφεται υποβολές.
|
||||
signing.wont_sign.parentsigned=Η υποβολή δε θα υπογραφεί καθώς η γονική υποβολή δεν έχει υπογραφεί.
|
||||
signing.wont_sign.basesigned=Η συγχώνευση δε θα υπογραφεί καθώς η βασική υποβολή δεν έχει υπογραφή της βάσης.
|
||||
signing.wont_sign.headsigned=Η συγχώνευση δε θα υπογραφεί καθώς δεν έχει υπογραφή η υποβολή της κεφαλής.
|
||||
signing.wont_sign.commitssigned=Η συγχώνευση δε θα υπογραφεί καθώς όλες οι σχετικές υποβολές δεν έχουν υπογραφεί.
|
||||
signing.wont_sign.approved=Η συγχώνευση δε θα υπογραφεί καθώς το PR δεν έχει εγκριθεί.
|
||||
signing.wont_sign.not_signed_in=Δεν είστε συνδεδεμένοι.
|
||||
|
||||
ext_wiki=Πρόσβαση στο Εξωτερικό Wiki
|
||||
|
@ -1968,7 +1999,9 @@ settings.mirror_settings.docs.disabled_push_mirror.info=Τα είδωλα ώθη
|
|||
settings.mirror_settings.docs.no_new_mirrors=Το αποθετήριο σας αντιγράφει τις αλλαγές προς ή από ένα άλλο αποθετήριο. Λάβετε υπόψη ότι δεν μπορείτε να δημιουργήσετε νέα είδωλα αυτή τη στιγμή.
|
||||
settings.mirror_settings.docs.can_still_use=Αν και δεν μπορείτε να τροποποιήσετε τα υπάρχοντα είδωλα ή να δημιουργήσετε νέα, μπορείτε να χρησιμοποιείται ακόμα το υπάρχων είδωλο.
|
||||
settings.mirror_settings.docs.pull_mirror_instructions=Για να ορίσετε έναν είδωλο έλξης, παρακαλούμε συμβουλευθείτε:
|
||||
settings.mirror_settings.docs.more_information_if_disabled=Μπορείτε να μάθετε περισσότερα για τα είδωλα ώθησης και έλξης εδώ:
|
||||
settings.mirror_settings.docs.doc_link_title=Πώς μπορώ να αντιγράψω αποθετήρια;
|
||||
settings.mirror_settings.docs.doc_link_pull_section=το κεφάλαιο "Pulling from a remote repository" της τεκμηρίωσης.
|
||||
settings.mirror_settings.docs.pulling_remote_title=Έλξη από ένα απομακρυσμένο αποθετήριο
|
||||
settings.mirror_settings.mirrored_repository=Είδωλο αποθετηρίου
|
||||
settings.mirror_settings.direction=Κατεύθυνση
|
||||
|
@ -1981,6 +2014,8 @@ settings.mirror_settings.push_mirror.add=Προσθήκη Είδωλου Push
|
|||
settings.mirror_settings.push_mirror.edit_sync_time=Επεξεργασία διαστήματος συγχρονισμού ειδώλου
|
||||
|
||||
settings.sync_mirror=Συγχρονισμός Τώρα
|
||||
settings.pull_mirror_sync_in_progress=Έλκονται αλλαγές από το απομακρυσμένο %s αυτή τη στιγμή.
|
||||
settings.push_mirror_sync_in_progress=Ώθηση αλλαγών στο απομακρυσμένο %s αυτή τη στιγμή.
|
||||
settings.site=Ιστοσελίδα
|
||||
settings.update_settings=Ενημέρωση Ρυθμίσεων
|
||||
settings.update_mirror_settings=Ενημέρωση Ρυθμίσεων Ειδώλου
|
||||
|
@ -2047,6 +2082,7 @@ settings.transfer.rejected=Η μεταβίβαση του αποθετηρίου
|
|||
settings.transfer.success=Η μεταβίβαση του αποθετηρίου ήταν επιτυχής.
|
||||
settings.transfer_abort=Ακύρωση μεταβίβασης
|
||||
settings.transfer_abort_invalid=Δεν μπορείτε να ακυρώσετε μια ανύπαρκτη μεταβίβαση αποθετηρίου.
|
||||
settings.transfer_abort_success=Η μεταφορά αποθετηρίου στο %s ακυρώθηκε με επιτυχία.
|
||||
settings.transfer_desc=Μεταβιβάστε αυτό το αποθετήριο σε έναν χρήστη ή σε έναν οργανισμό για τον οποίο έχετε δικαιώματα διαχειριστή.
|
||||
settings.transfer_form_title=Εισάγετε το όνομα του αποθετηρίου ως επιβεβαίωση:
|
||||
settings.transfer_in_progress=Αυτή τη στιγμή υπάρχει μια εν εξελίξει μεταβίβαση. Παρακαλούμε ακυρώστε την αν θέλετε να μεταβιβάσετε αυτό το αποθετήριο σε άλλο χρήστη.
|
||||
|
@ -2337,6 +2373,7 @@ settings.unarchive.button=Απο-Αρχειοθέτηση αποθετηρίου
|
|||
settings.unarchive.header=Απο-Αρχειοθέτηση του αποθετηρίου
|
||||
settings.unarchive.text=Η απο-αρχειοθέτηση του αποθετηρίου θα αποκαταστήσει την ικανότητά του να λαμβάνει υποβολές και ωθήσεις, καθώς και νέα ζητήματα και pull-requests.
|
||||
settings.unarchive.success=Το αποθετήριο απο-αρχειοθετήθηκε με επιτυχία.
|
||||
settings.unarchive.error=Παρουσιάστηκε σφάλμα κατά την προσπάθεια απο-αρχειοθέτησης του αποθετηρίου. Δείτε τις καταγραφές για περισσότερες λεπτομέρειες.
|
||||
settings.update_avatar_success=Η εικόνα του αποθετηρίου έχει ενημερωθεί.
|
||||
settings.lfs=LFS
|
||||
settings.lfs_filelist=Αρχεία LFS σε αυτό το αποθετήριο
|
||||
|
@ -2460,6 +2497,7 @@ release.edit_release=Ενημέρωση Κυκλοφορίας
|
|||
release.delete_release=Διαγραφή Κυκλοφορίας
|
||||
release.delete_tag=Διαγραφή Ετικέτας
|
||||
release.deletion=Διαγραφή Κυκλοφορίας
|
||||
release.deletion_desc=Διαγράφοντας μια κυκλοφορία, αυτή αφαιρείται μόνο από το Gitea. Δε θα επηρεάσει την ετικέτα Git, τα περιεχόμενα του αποθετηρίου σας ή το ιστορικό της. Συνέχεια;
|
||||
release.deletion_success=Η κυκλοφορία έχει διαγραφεί.
|
||||
release.deletion_tag_desc=Θα διαγράψει αυτή την ετικέτα από το αποθετήριο. Τα περιεχόμενα του αποθετηρίου και το ιστορικό παραμένουν αμετάβλητα. Συνέχεια;
|
||||
release.deletion_tag_success=Η ετικέτα έχει διαγραφεί.
|
||||
|
@ -2479,6 +2517,7 @@ branch.already_exists=Ήδη υπάρχει ένας κλάδος με το όν
|
|||
branch.delete_head=Διαγραφή
|
||||
branch.delete=`Διαγραφή του Κλάδου "%s"`
|
||||
branch.delete_html=Διαγραφή Κλάδου
|
||||
branch.delete_desc=Η διαγραφή ενός κλάδου είναι μόνιμη. Αν και ο διαγραμμένος κλάδος μπορεί να συνεχίσει να υπάρχει για σύντομο χρονικό διάστημα πριν να αφαιρεθεί, ΔΕΝ ΜΠΟΡΕΙ να αναιρεθεί στις περισσότερες περιπτώσεις. Συνέχεια;
|
||||
branch.deletion_success=Ο κλάδος "%s" διαγράφηκε.
|
||||
branch.deletion_failed=Αποτυχία διαγραφής του κλάδου "%s".
|
||||
branch.delete_branch_has_new_commits=Ο κλάδος "%s" δεν μπορεί να διαγραφεί επειδή προστέθηκαν νέες υποβολές μετά τη συγχώνευση.
|
||||
|
@ -2713,6 +2752,7 @@ dashboard.reinit_missing_repos=Επανεκκινήστε όλα τα αποθε
|
|||
dashboard.sync_external_users=Συγχρονισμός δεδομένων εξωτερικών χρηστών
|
||||
dashboard.cleanup_hook_task_table=Εκκαθάριση πίνακα hook_task
|
||||
dashboard.cleanup_packages=Εκκαθάριση ληγμένων πακέτων
|
||||
dashboard.cleanup_actions=Οι ενέργειες καθαρισμού καταγραφές και αντικείμενα
|
||||
dashboard.server_uptime=Διάρκεια Διακομιστή
|
||||
dashboard.current_goroutine=Τρέχουσες Goroutines
|
||||
dashboard.current_memory_usage=Τρέχουσα Χρήση Μνήμης
|
||||
|
@ -2823,6 +2863,7 @@ emails.updated=Το email ενημερώθηκε
|
|||
emails.not_updated=Αποτυχία ενημέρωσης της ζητούμενης διεύθυνσης email: %v
|
||||
emails.duplicate_active=Αυτή η διεύθυνση email είναι ήδη ενεργή σε διαφορετικό χρήστη.
|
||||
emails.change_email_header=Ενημέρωση Ιδιοτήτων Email
|
||||
emails.change_email_text=Είστε βέβαιοι ότι θέλετε να ενημερώσετε αυτή τη διεύθυνση email;
|
||||
|
||||
orgs.org_manage_panel=Διαχείριση Οργανισμού
|
||||
orgs.name=Όνομα
|
||||
|
@ -2847,6 +2888,7 @@ packages.package_manage_panel=Διαχείριση Πακέτων
|
|||
packages.total_size=Συνολικό Μέγεθος: %s
|
||||
packages.unreferenced_size=Μέγεθος Χωρίς Αναφορά: %s
|
||||
packages.cleanup=Εκκαθάριση ληγμένων δεδομένων
|
||||
packages.cleanup.success=Επιτυχής εκκαθάριση δεδομένων που έχουν λήξει
|
||||
packages.owner=Ιδιοκτήτης
|
||||
packages.creator=Δημιουργός
|
||||
packages.name=Όνομα
|
||||
|
@ -2857,10 +2899,12 @@ packages.size=Μέγεθος
|
|||
packages.published=Δημοσιευμένα
|
||||
|
||||
defaulthooks=Προεπιλεγμένα Webhooks
|
||||
defaulthooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούν ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ είναι προκαθορισμένα και θα αντιγραφούν σε όλα τα νέα αποθετήρια. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>.
|
||||
defaulthooks.add_webhook=Προσθήκη Προεπιλεγμένου Webhook
|
||||
defaulthooks.update_webhook=Ενημέρωση Προεπιλεγμένου Webhook
|
||||
|
||||
systemhooks=Webhooks Συστήματος
|
||||
systemhooks.desc=Τα Webhooks κάνουν αυτόματα αιτήσεις HTTP POST σε ένα διακομιστή όταν ενεργοποιούνται ορισμένα γεγονότα στο Gitea. Τα Webhooks που ορίζονται εδώ θα ενεργούν σε όλα τα αποθετήρια του συστήματος, γι 'αυτό παρακαλώ εξετάστε τυχόν επιπτώσεις απόδοσης που μπορεί να έχει. Διαβάστε περισσότερα στον οδηγό <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">webhooks</a>.
|
||||
systemhooks.add_webhook=Προσθήκη Webhook Συστήματος
|
||||
systemhooks.update_webhook=Ενημέρωση Webhook Συστήματος
|
||||
|
||||
|
@ -2953,6 +2997,7 @@ auths.sspi_default_language=Προεπιλεγμένη γλώσσα χρήστη
|
|||
auths.sspi_default_language_helper=Προεπιλεγμένη γλώσσα για τους χρήστες που δημιουργούνται αυτόματα με τη μέθοδο ταυτοποίησης SSPI. Αφήστε κενό αν προτιμάτε η γλώσσα να εντοπιστεί αυτόματα.
|
||||
auths.tips=Συμβουλές
|
||||
auths.tips.oauth2.general=Ταυτοποίηση OAuth2
|
||||
auths.tips.oauth2.general.tip=Κατά την εγγραφή μιας νέας ταυτοποίησης OAuth2, το URL κλήσης/ανακατεύθυνσης πρέπει να είναι:
|
||||
auths.tip.oauth2_provider=Πάροχος OAuth2
|
||||
auths.tip.bitbucket=Καταχωρήστε ένα νέο καταναλωτή OAuth στο https://bitbucket.org/account/user/<your username>/oauth-consumers/new και προσθέστε το δικαίωμα 'Account' - 'Read'
|
||||
auths.tip.nextcloud=`Καταχωρήστε ένα νέο καταναλωτή OAuth στην υπηρεσία σας χρησιμοποιώντας το παρακάτω μενού "Settings -> Security -> OAuth 2.0 client"`
|
||||
|
@ -2964,6 +3009,7 @@ auths.tip.google_plus=Αποκτήστε τα διαπιστευτήρια πε
|
|||
auths.tip.openid_connect=Χρησιμοποιήστε το OpenID Connect Discovery URL (<server>/.well known/openid-configuration) για να καθορίσετε τα τελικά σημεία
|
||||
auths.tip.twitter=Πηγαίνετε στο https://dev.twitter.com/apps, δημιουργήστε μια εφαρμογή και βεβαιωθείτε ότι η επιλογή “Allow this application to be used to Sign in with Twitter” είναι ενεργοποιημένη
|
||||
auths.tip.discord=Καταχωρήστε μια νέα εφαρμογή στο https://discordapp.com/developers/applications/me
|
||||
auths.tip.gitea=Καταχωρήστε μια νέα εφαρμογή OAuth2. Μπορείτε να βρείτε τον οδηγό στο https://docs.gitea.com/development/oauth2-provider
|
||||
auths.tip.yandex=`Δημιουργήστε μια νέα εφαρμογή στο https://oauth.yandex.com/client/new. Επιλέξτε τα ακόλουθα δικαιώματα από την ενότητα "Yandex.Passport API": "Access to email address", "Access to user avatar" και "Access to username, first name and surname, gender"`
|
||||
auths.tip.mastodon=Εισαγάγετε ένα προσαρμομένο URL για την υπηρεσία mastodon με την οποία θέλετε να πιστοποιήσετε (ή να χρησιμοποιήσετε την προεπιλεγμένη)
|
||||
auths.edit=Επεξεργασία Πηγής Ταυτοποίησης
|
||||
|
@ -3267,6 +3313,7 @@ desc=Διαχείριση πακέτων μητρώου.
|
|||
empty=Δεν υπάρχουν πακέτα ακόμα.
|
||||
empty.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο πακέτων, ανατρέξτε <a target="_blank" rel="noopener noreferrer" href="%s">στην τεκμηρίωση</a>.
|
||||
empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις <a href="%[1]s">ρυθμίσεις πακέτων</a> και συνδέστε το σε αυτό το αποθετήριο.
|
||||
registry.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο %s, ανατρέξτε στη τεκμηρίωση <a target="_blank" rel="noopener noreferrer" href="%s"></a>.
|
||||
filter.type=Τύπος
|
||||
filter.type.all=Όλα
|
||||
filter.no_result=Το φίλτρο δεν παρήγαγε αποτελέσματα.
|
||||
|
@ -3378,9 +3425,11 @@ settings.delete.success=Το πακέτο έχει διαγραφεί.
|
|||
settings.delete.error=Αποτυχία διαγραφής του πακέτου.
|
||||
owner.settings.cargo.title=Ευρετήριο Μητρώου Cargo
|
||||
owner.settings.cargo.initialize=Αρχικοποίηση Ευρετηρίου
|
||||
owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό αποθετήριο ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το αποθετήριο και θα ρυθμιστεί αυτόματα.
|
||||
owner.settings.cargo.initialize.error=Αποτυχία αρχικοποίησης ευρετηρίου Cargo: %v
|
||||
owner.settings.cargo.initialize.success=Ο ευρετήριο Cargo δημιουργήθηκε με επιτυχία.
|
||||
owner.settings.cargo.rebuild=Αναδημιουργία Ευρετηρίου
|
||||
owner.settings.cargo.rebuild.description=Η ανοικοδόμηση μπορεί να είναι χρήσιμη εάν ο δείκτης δεν είναι συγχρονισμένος με τα αποθηκευμένα πακέτα Cargo.
|
||||
owner.settings.cargo.rebuild.error=Αποτυχία αναδόμησης του ευρετηρίου Cargo: %v
|
||||
owner.settings.cargo.rebuild.success=Το ευρετήριο Cargo αναδομήθηκε με επιτυχία.
|
||||
owner.settings.cleanuprules.title=Διαχείριση Κανόνων Εκκαθάρισης
|
||||
|
@ -3405,6 +3454,7 @@ owner.settings.cleanuprules.success.update=Ο κανόνας καθαρισμο
|
|||
owner.settings.cleanuprules.success.delete=Ο κανόνας καθαρισμού διαγράφηκε.
|
||||
owner.settings.chef.title=Μητρώο Chef
|
||||
owner.settings.chef.keypair=Δημιουργία ζεύγους κλειδιών
|
||||
owner.settings.chef.keypair.description=Ένα ζεύγος κλειδιών είναι απαραίτητο για ταυτοποίηση στο μητρώο Chef. Αν έχετε δημιουργήσει ένα ζεύγος κλειδιών πριν, η δημιουργία ενός νέου ζεύγους κλειδιών θα απορρίψει το παλιό ζεύγος κλειδιών.
|
||||
|
||||
[secrets]
|
||||
secrets=Μυστικά
|
||||
|
|
|
@ -1956,6 +1956,8 @@ wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: '
|
|||
wiki.original_git_entry_tooltip = View original Git file instead of using friendly link.
|
||||
|
||||
activity = Activity
|
||||
activity.navbar.pulse = Pulse
|
||||
activity.navbar.contributors = Contributors
|
||||
activity.period.filter_label = Period:
|
||||
activity.period.daily = 1 day
|
||||
activity.period.halfweekly = 3 days
|
||||
|
@ -2021,6 +2023,16 @@ activity.git_stats_and_deletions = and
|
|||
activity.git_stats_deletion_1 = %d deletion
|
||||
activity.git_stats_deletion_n = %d deletions
|
||||
|
||||
contributors = Contributors
|
||||
contributors.contribution_type.filter_label = Contribution type:
|
||||
contributors.contribution_type.commits = Commits
|
||||
contributors.contribution_type.additions = Additions
|
||||
contributors.contribution_type.deletions = Deletions
|
||||
contributors.loading_title = Loading contributions...
|
||||
contributors.loading_title_failed = Could not load contributions
|
||||
contributors.loading_info = This might take a bit…
|
||||
contributors.component_failed_to_load = An unexpected error happened.
|
||||
|
||||
search = Search
|
||||
search.search_repo = Search repository
|
||||
search.type.tooltip = Search type
|
||||
|
|
|
@ -18,6 +18,7 @@ template=Şablon
|
|||
language=Dil
|
||||
notifications=Bildirimler
|
||||
active_stopwatch=Etkin Zaman Takibi
|
||||
tracked_time_summary=Konu listesi süzgeçlerine dayanan takip edilen zamanın özeti
|
||||
create_new=Oluştur…
|
||||
user_profile_and_more=Profil ve Ayarlar…
|
||||
signed_in_as=Giriş yapan:
|
||||
|
@ -91,6 +92,7 @@ remove=Kaldır
|
|||
remove_all=Tümünü Kaldır
|
||||
remove_label_str=`"%s" öğesini kaldır`
|
||||
edit=Düzenle
|
||||
view=Görüntüle
|
||||
|
||||
enabled=Aktifleştirilmiş
|
||||
disabled=Devre Dışı
|
||||
|
@ -98,6 +100,7 @@ locked=Kilitli
|
|||
|
||||
copy=Kopyala
|
||||
copy_url=URL'yi kopyala
|
||||
copy_hash=Hash'i kopyala
|
||||
copy_content=İçeriği kopyala
|
||||
copy_branch=Dal adını kopyala
|
||||
copy_success=Kopyalandı!
|
||||
|
@ -110,6 +113,7 @@ loading=Yükleniyor…
|
|||
|
||||
error=Hata
|
||||
error404=Ulaşmaya çalıştığınız sayfa <strong>mevcut değil</strong> veya <strong>görüntüleme yetkiniz yok</strong>.
|
||||
go_back=Geri Git
|
||||
|
||||
never=Asla
|
||||
unknown=Bilinmiyor
|
||||
|
@ -181,6 +185,7 @@ network_error=Ağ hatası
|
|||
[startpage]
|
||||
app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi
|
||||
install=Kurulumu kolay
|
||||
install_desc=Platformunuz için <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">ikili dosyayı çalıştırın</a>, <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a> ile yükleyin veya <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">paket</a> olarak edinin.
|
||||
platform=Farklı platformlarda çalışablir
|
||||
platform_desc=Forgejo <a target="_blank" rel="noopener noreferrer" href="http://golang.org/">Go</a> ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin!
|
||||
lightweight=Hafif
|
||||
|
@ -357,6 +362,7 @@ disable_register_prompt=Kayıt işlemi devre dışıdır. Lütfen site yönetici
|
|||
disable_register_mail=Kayıt için e-posta doğrulama devre dışıdır.
|
||||
manual_activation_only=Etkinleştirmeyi tamamlamak için site yöneticinizle bağlantıya geçin.
|
||||
remember_me=Bu Aygıtı hatırla
|
||||
remember_me.compromised=Oturum açma tokeni artık geçerli değil, bu ele geçirilmiş bir hesaba işaret ediyor olabilir. Lütfen hesabınızda olağandışı faaliyet olup olmadığını denetleyin.
|
||||
forgot_password_title=Şifremi unuttum
|
||||
forgot_password=Şifrenizi mi unuttunuz?
|
||||
sign_up_now=Bir hesaba mı ihtiyacınız var? Hemen kaydolun.
|
||||
|
@ -376,6 +382,7 @@ email_not_associate=Bu e-posta adresi hiçbir hesap ile ilişkilendirilmemiştir
|
|||
send_reset_mail=Hesap Kurtarma E-postası Gönder
|
||||
reset_password=Hesap Kurtarma
|
||||
invalid_code=Doğrulama kodunuz geçersiz veya süresi dolmuş.
|
||||
invalid_code_forgot_password=Onay kodunuz hatalı veya süresi geçmiş. Yeni bir oturum başlatmak için <a href="%s">buraya</a> tıklayın.
|
||||
invalid_password=Parolanız hesap oluşturulurken kullanılan parolayla eşleşmiyor.
|
||||
reset_password_helper=Hesabı Kurtar
|
||||
reset_password_wrong_user=%s olarak oturum açmışsınız, ancak hesap kurtarma bağlantısı %s için
|
||||
|
@ -677,6 +684,7 @@ choose_new_avatar=Yeni Avatar Seç
|
|||
update_avatar=Profil Resmini Güncelle
|
||||
delete_current_avatar=Güncel Avatarı Sil
|
||||
uploaded_avatar_not_a_image=Yüklenen dosya bir resim dosyası değil.
|
||||
uploaded_avatar_is_too_big=Yüklenen dosyanın boyutu (%d KiB), azami boyutu (%d KiB) aşıyor.
|
||||
update_avatar_success=Profil resminiz değiştirildi.
|
||||
update_user_avatar_success=Kullanıcının avatarı güncellendi.
|
||||
|
||||
|
@ -858,6 +866,7 @@ revoke_oauth2_grant_description=Bu üçüncü taraf uygulamasına erişimin ipta
|
|||
revoke_oauth2_grant_success=Erişim başarıyla kaldırıldı.
|
||||
|
||||
twofa_desc=İki faktörlü kimlik doğrulama, hesabınızın güvenliğini artırır.
|
||||
twofa_recovery_tip=Aygıtınızı kaybetmeniz durumunda, hesabınıza tekrar erişmek için tek kullanımlık kurtarma anahtarını kullanabileceksiniz.
|
||||
twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde <strong>kaydedilmiş</strong>.
|
||||
twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş.
|
||||
twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak
|
||||
|
@ -880,6 +889,8 @@ webauthn_register_key=Güvenlik Anahtarı Ekle
|
|||
webauthn_nickname=Takma Ad
|
||||
webauthn_delete_key=Güvenlik Anahtarını Kaldır
|
||||
webauthn_delete_key_desc=Bir güvenlik anahtarını kaldırırsanız, onunla artık giriş yapamazsınız. Devam edilsin mi?
|
||||
webauthn_key_loss_warning=Güvenlik anahtarlarınızı kaybederseniz, hesabınıza erişimi kaybedersiniz.
|
||||
webauthn_alternative_tip=Ek bir kimlik doğrulama yöntemi ayarlamak isteyebilirsiniz.
|
||||
|
||||
manage_account_links=Bağlı Hesapları Yönet
|
||||
manage_account_links_desc=Bu harici hesaplar Forgejo hesabınızla bağlantılı.
|
||||
|
@ -916,6 +927,7 @@ visibility.private=Özel
|
|||
visibility.private_tooltip=Sadece katıldığınız organizasyonların üyeleri tarafından görünür
|
||||
|
||||
[repo]
|
||||
new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içerir. Zaten başka bir yerde mi barındırıyorsunuz? <a href="%s">Depoyu taşıyın.</a>
|
||||
owner=Sahibi
|
||||
owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir.
|
||||
repo_name=Depo İsmi
|
||||
|
@ -936,6 +948,8 @@ fork_from=Buradan Çatalla
|
|||
already_forked=%s deposunu zaten çatalladınız
|
||||
fork_to_different_account=Başka bir hesaba çatalla
|
||||
fork_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez.
|
||||
fork_branch=Çatala klonlanacak dal
|
||||
all_branches=Tüm dallar
|
||||
fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz.
|
||||
use_template=Bu şablonu kullan
|
||||
clone_in_vsc=VS Code'ta klonla
|
||||
|
@ -965,6 +979,7 @@ trust_model_helper_collaborator_committer=Ortak çalışan+İşleyen: İşleyenl
|
|||
trust_model_helper_default=Varsayılan: Bu kurulum için varsayılan güven modelini kullan
|
||||
create_repo=Depo Oluştur
|
||||
default_branch=Varsayılan Dal
|
||||
default_branch_label=varsayılan
|
||||
default_branch_helper=Varsayılan dal, değişiklik istekleri ve kod işlemeleri için temel daldır.
|
||||
mirror_prune=Buda
|
||||
mirror_prune_desc=Kullanılmayan uzak depoları izleyen referansları kaldır
|
||||
|
@ -1000,8 +1015,13 @@ delete_preexisting=Önceden var olan dosyaları sil
|
|||
delete_preexisting_content=%s içindeki dosyaları sil
|
||||
delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi
|
||||
blame_prior=Bu değişiklikten önceki suçu görüntüle
|
||||
blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için <a href="%s">buraya tıklayın</a>.
|
||||
blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> dosyasındaki sürümler yok sayılamadı.
|
||||
author_search_tooltip=En fazla 30 kullanıcı görüntüler
|
||||
|
||||
tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil
|
||||
tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil
|
||||
tree_path_not_found_tag=%[1] yolu, %[2]s etiketinde mevcut değil
|
||||
|
||||
transfer.accept=Aktarımı Kabul Et
|
||||
transfer.accept_desc=`"%s" tarafına aktar`
|
||||
|
@ -1265,6 +1285,7 @@ commits.signed_by_untrusted_user=Güvenilmeyen kullanıcı tarafından imzaland
|
|||
commits.signed_by_untrusted_user_unmatched=İşleyici ile eşleşmeyen güvenilmeyen kullanıcı tarafından imzalanmış
|
||||
commits.gpg_key_id=GPG Anahtar Kimliği
|
||||
commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi
|
||||
commits.view_path=Geçmişte bu noktayı görüntüle
|
||||
|
||||
commit.operations=İşlemler
|
||||
commit.revert=Geri Al
|
||||
|
@ -1475,8 +1496,17 @@ issues.ref_closed_from=`<a href="%[3]s">bu konuyu kapat%[4]s</a> <a id="%[1]s" h
|
|||
issues.ref_reopened_from=`<a href="%[3]s">konuyu yeniden aç%[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
issues.ref_from=`%[1]s'den`
|
||||
issues.author=Yazar
|
||||
issues.author_helper=Bu kullanıcı yazardır.
|
||||
issues.role.owner=Sahibi
|
||||
issues.role.owner_helper=Bu kullanıcı bu deponun sahibidir.
|
||||
issues.role.member=Üye
|
||||
issues.role.member_helper=Bu kullanıcı bu deponun sahibi olan organizasyonun üyesidir.
|
||||
issues.role.collaborator=Katkıcı
|
||||
issues.role.collaborator_helper=Kullanıcı bu depoya işbirliği için davet edildi.
|
||||
issues.role.first_time_contributor=İlk defa katkıcı
|
||||
issues.role.first_time_contributor_helper=Bu, bu kullanıcının bu depoya ilk katkısı.
|
||||
issues.role.contributor=Katılımcı
|
||||
issues.role.contributor_helper=Bu kullanıcı bu depoya daha önce işleme gönderdi.
|
||||
issues.re_request_review=İncelemeyi yeniden iste
|
||||
issues.is_stale=Bu incelemeden bu yana bu istekte değişiklikler oldu
|
||||
issues.remove_request_review=İnceleme isteğini kaldır
|
||||
|
@ -1492,6 +1522,8 @@ issues.label_description=Etiket açıklaması
|
|||
issues.label_color=Etiket rengi
|
||||
issues.label_exclusive=Özel
|
||||
issues.label_archive=Etiketi Arşivle
|
||||
issues.label_archived_filter=Arşivlenmiş etiketleri göster
|
||||
issues.label_archive_tooltip=Arşivlenmiş etiketler, etiket araması yapılırken varsayılan olarak önerilerin dışında tutuluyor.
|
||||
issues.label_exclusive_desc=<code>Kapsam/öğe</code> etiketini, diğer <code>kapsam/</code> etiketleriyle ayrışık olacak şekilde adlandırın.
|
||||
issues.label_exclusive_warning=Çakışan kapsamlı etiketler, bir konu veya değişiklik isteği etiketleri düzenlenirken kaldırılacaktır.
|
||||
issues.label_count=%d etiket
|
||||
|
@ -1746,6 +1778,7 @@ pulls.rebase_conflict_summary=Hata Mesajı
|
|||
pulls.unrelated_histories=Birleştirme Başarısız: Birleştirme başlığı ve tabanı ortak bir geçmişi paylaşmıyor. İpucu: Farklı bir strateji deneyin
|
||||
pulls.merge_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, taban güncellendi. İpucu: Tekrar deneyin.
|
||||
pulls.head_out_of_date=Birleştirme Başarısız: Birleştirme oluşturulurken, ana güncellendi. İpucu: Tekrar deneyin.
|
||||
pulls.has_merged=Başarısız: Değişiklik isteği birleştirildi, yeniden birleştiremez veya hedef dalı değiştiremezsiniz.
|
||||
pulls.push_rejected=Birleştirme Başarısız Oldu: Gönderme reddedildi. Bu depo için Git İstemcilerini inceleyin.
|
||||
pulls.push_rejected_summary=Tam Red Mesajı
|
||||
pulls.push_rejected_no_message=Birleştirme başarısız oldu: Gönderme reddedildi, ancak uzak bir mesaj yoktu.<br>Bu depo için Git İstemcilerini inceleyin
|
||||
|
@ -1757,6 +1790,8 @@ pulls.status_checks_failure=Bazı kontroller başarısız oldu
|
|||
pulls.status_checks_error=Bazı kontroller hatalar bildirdi
|
||||
pulls.status_checks_requested=Gerekli
|
||||
pulls.status_checks_details=Ayrıntılar
|
||||
pulls.status_checks_hide_all=Tüm denetlemeleri gizle
|
||||
pulls.status_checks_show_all=Tüm denetlemeleri göster
|
||||
pulls.update_branch=Dalı birleştirmeyle güncelle
|
||||
pulls.update_branch_rebase=Dalı yeniden yapılandırmayla güncelle
|
||||
pulls.update_branch_success=Dal güncellemesi başarıyla gerçekleştirildi
|
||||
|
@ -1765,6 +1800,11 @@ pulls.outdated_with_base_branch=Bu dal, temel dal ile güncel değil
|
|||
pulls.close=Değişiklik İsteğini Kapat
|
||||
pulls.closed_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini kapattı`
|
||||
pulls.reopened_at=`<a id="%[1]s" href="#%[1]s">%[2]s</a> değişiklik isteğini yeniden açtı`
|
||||
pulls.cmd_instruction_hint=`<a class="show-instruction">Komut satırı talimatlarını</a> görüntüleyin.`
|
||||
pulls.cmd_instruction_checkout_title=Çekme
|
||||
pulls.cmd_instruction_checkout_desc=Proje deponuzdan yeni bir dalı çekin ve değişiklikleri test edin.
|
||||
pulls.cmd_instruction_merge_title=Birleştir
|
||||
pulls.cmd_instruction_merge_desc=Değişiklikleri birleştirin ve Gitea'da güncelleyin.
|
||||
pulls.clear_merge_message=Birleştirme iletilerini temizle
|
||||
pulls.clear_merge_message_hint=Birleştirme iletisini temizlemek sadece işleme ileti içeriğini kaldırır ama üretilmiş "Co-Authored-By …" gibi git fragmanlarını korur.
|
||||
|
||||
|
@ -1810,6 +1850,8 @@ milestones.edit_success=`"%s" dönüm noktası güncellendi.`
|
|||
milestones.deletion=Kilometre Taşını Sil
|
||||
milestones.deletion_desc=Bir kilometre taşını silmek, onu ilgili tüm sorunlardan kaldırır. Devam edilsin mi?
|
||||
milestones.deletion_success=Kilometre taşı silindi.
|
||||
milestones.filter_sort.earliest_due_data=En erken bitiş tarihi
|
||||
milestones.filter_sort.latest_due_date=En uzak bitiş tarihi
|
||||
milestones.filter_sort.least_complete=En az tamamlama
|
||||
milestones.filter_sort.most_complete=En çok tamamlama
|
||||
milestones.filter_sort.most_issues=En çok konu
|
||||
|
@ -1972,6 +2014,8 @@ settings.mirror_settings.push_mirror.add=Yansı Gönderimi Ekle
|
|||
settings.mirror_settings.push_mirror.edit_sync_time=Yansı eşzamanlama aralığını düzenle
|
||||
|
||||
settings.sync_mirror=Şimdi Eşitle
|
||||
settings.pull_mirror_sync_in_progress=Şu an %s uzak sunucusundan değişiklikler çekiliyor.
|
||||
settings.push_mirror_sync_in_progress=Şu an %s uzak sunucusuna değişiklikler itiliyor.
|
||||
settings.site=Web Sitesi
|
||||
settings.update_settings=Ayarları Güncelle
|
||||
settings.update_mirror_settings=Yansı Ayarları Güncelle
|
||||
|
@ -2105,12 +2149,14 @@ settings.webhook_deletion_desc=Bir web isteğini kaldırmak, ayarlarını ve tes
|
|||
settings.webhook_deletion_success=Web isteği silindi.
|
||||
settings.webhook.test_delivery=Test Dağıtımı
|
||||
settings.webhook.test_delivery_desc=Bu web isteğini sahte bir olayla test edin.
|
||||
settings.webhook.test_delivery_desc_disabled=Bu web istemcisini sahte bir olayla denemek için etkinleştirin.
|
||||
settings.webhook.request=İstekler
|
||||
settings.webhook.response=Cevaplar
|
||||
settings.webhook.headers=Başlıklar
|
||||
settings.webhook.payload=İçerik
|
||||
settings.webhook.body=Gövde
|
||||
settings.webhook.replay.description=Bu web kancasını tekrar çalıştır.
|
||||
settings.webhook.replay.description_disabled=Bu web istemcisini yeniden oynatmak için etkinleştirin.
|
||||
settings.webhook.delivery.success=Teslim kuyruğuna bir olay eklendi. Teslim geçmişinde görünmesi birkaç saniye alabilir.
|
||||
settings.githooks_desc=Git İstemcileri Git'in kendisi tarafından desteklenmektedir. Özel işlemler ayarlamak için aşağıdaki istemci dosyalarını düzenleyebilirsiniz.
|
||||
settings.githook_edit_desc=İstek aktif değilse örnek içerik sunulacaktır. İçeriği boş bırakmak, isteği devre dışı bırakmayı beraberinde getirecektir.
|
||||
|
@ -2271,6 +2317,7 @@ settings.dismiss_stale_approvals_desc=Değişiklik isteğinin içeriğini deği
|
|||
settings.require_signed_commits=İmzalı İşleme Gerekli
|
||||
settings.require_signed_commits_desc=Reddetme, onlar imzasızsa veya doğrulanamazsa bu dala gönderir.
|
||||
settings.protect_branch_name_pattern=Korunmuş Dal Adı Deseni
|
||||
settings.protect_branch_name_pattern_desc=Korunmuş dal isim desenleri. Desen sözdizimi için <a href="https://github.com/gobwas/glob">belgelere</a> bakabilirsiniz. Örnekler: main, release/**
|
||||
settings.protect_patterns=Desenler
|
||||
settings.protect_protected_file_patterns=Korumalı dosya kalıpları (noktalı virgülle ayrılmış ';'):
|
||||
settings.protect_protected_file_patterns_desc=Kullanıcının bu dalda dosya ekleme, düzenleme veya silme hakları olsa bile doğrudan değiştirilmesine izin verilmeyen korumalı dosyalar. Birden çok desen noktalı virgül (';') kullanılarak ayrılabilir. Desen sözdizimi için <a href='https://pkg.go.dev/github.com/gobwas/glob#Compile'>github.com/gobwas/glob</a> belgelerine bakın. Örnekler: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>.
|
||||
|
@ -2307,6 +2354,7 @@ settings.tags.protection.allowed.teams=İzin verilen takımlar
|
|||
settings.tags.protection.allowed.noone=Hiç kimse
|
||||
settings.tags.protection.create=Etiketi Koru
|
||||
settings.tags.protection.none=Korumalı etiket yok.
|
||||
settings.tags.protection.pattern.description=Birden çok etiketi eşleştirmek için tek bir ad, glob deseni veya normal ifade kullanabilirsiniz. Daha fazlası için <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">korumalı etiketler rehberini</a> okuyun.
|
||||
settings.bot_token=Bot Jetonu
|
||||
settings.chat_id=Sohbet Kimliği
|
||||
settings.thread_id=İş Parçacığı ID
|
||||
|
@ -2487,6 +2535,7 @@ branch.default_deletion_failed=`"%s" dalı varsayılan daldır. Silinemez.`
|
|||
branch.restore=`"%s" Dalını Geri Yükle`
|
||||
branch.download=`"%s" Dalını İndir`
|
||||
branch.rename=`"%s" Dalının Adını Değiştir`
|
||||
branch.search=Dal Ara
|
||||
branch.included_desc=Bu dal varsayılan dalın bir parçasıdır
|
||||
branch.included=Dahil
|
||||
branch.create_new_branch=Şu daldan dal oluştur:
|
||||
|
@ -2703,6 +2752,7 @@ dashboard.reinit_missing_repos=Kayıtları bulunanlar için tüm eksik Git depol
|
|||
dashboard.sync_external_users=Harici kullanıcı verisini senkronize et
|
||||
dashboard.cleanup_hook_task_table=Hook_task tablosunu temizleme
|
||||
dashboard.cleanup_packages=Süresi dolmuş paketleri temizleme
|
||||
dashboard.cleanup_actions=Eylemlerin süresi geçmiş günlük ve yapılarını temizle
|
||||
dashboard.server_uptime=Sunucunun Ayakta Kalma Süresi
|
||||
dashboard.current_goroutine=Güncel Goroutine'ler
|
||||
dashboard.current_memory_usage=Güncel Bellek Kullanımı
|
||||
|
@ -2740,7 +2790,9 @@ dashboard.gc_lfs=LFS üst nesnelerin atıklarını temizle
|
|||
dashboard.stop_zombie_tasks=Zombi görevleri durdur
|
||||
dashboard.stop_endless_tasks=Daimi görevleri durdur
|
||||
dashboard.cancel_abandoned_jobs=Terkedilmiş görevleri iptal et
|
||||
dashboard.start_schedule_tasks=Zamanlanmış görevleri başlat
|
||||
dashboard.sync_branch.started=Dal Eşzamanlaması başladı
|
||||
dashboard.rebuild_issue_indexer=Konu indeksini yeniden oluştur
|
||||
|
||||
users.user_manage_panel=Kullanıcı Hesap Yönetimi
|
||||
users.new_account=Yeni Kullanıcı Hesabı
|
||||
|
@ -2749,6 +2801,9 @@ users.full_name=Tam İsim
|
|||
users.activated=Aktifleştirilmiş
|
||||
users.admin=Yönetici
|
||||
users.restricted=Kısıtlanmış
|
||||
users.reserved=Rezerve
|
||||
users.bot=Bot
|
||||
users.remote=Uzak
|
||||
users.2fa=2FD
|
||||
users.repos=Depolar
|
||||
users.created=Oluşturuldu
|
||||
|
@ -2795,6 +2850,7 @@ users.list_status_filter.is_prohibit_login=Oturum Açmayı Önle
|
|||
users.list_status_filter.not_prohibit_login=Oturum Açmaya İzin Ver
|
||||
users.list_status_filter.is_2fa_enabled=2FA Etkin
|
||||
users.list_status_filter.not_2fa_enabled=2FA Devre Dışı
|
||||
users.details=Kullanıcı Ayrıntıları
|
||||
|
||||
emails.email_manage_panel=Kullanıcı E-posta Yönetimi
|
||||
emails.primary=Birincil
|
||||
|
@ -2807,6 +2863,7 @@ emails.updated=E-posta güncellendi
|
|||
emails.not_updated=İstenen e-posta adresi güncellenemedi: %v
|
||||
emails.duplicate_active=Bu e-posta adresi farklı bir kullanıcı için zaten aktif.
|
||||
emails.change_email_header=E-posta Özelliklerini Güncelle
|
||||
emails.change_email_text=Bu e-posta adresini güncellemek istediğinizden emin misiniz?
|
||||
|
||||
orgs.org_manage_panel=Organizasyon Yönetimi
|
||||
orgs.name=İsim
|
||||
|
@ -2831,6 +2888,7 @@ packages.package_manage_panel=Paket Yönetimi
|
|||
packages.total_size=Toplam Boyut: %s
|
||||
packages.unreferenced_size=Referanssız Boyut: %s
|
||||
packages.cleanup=Süresi dolmuş veriyi temizle
|
||||
packages.cleanup.success=Süresi dolmuş veri başarıyla temizlendi
|
||||
packages.owner=Sahibi
|
||||
packages.creator=Oluşturan
|
||||
packages.name=İsim
|
||||
|
@ -2841,10 +2899,12 @@ packages.size=Boyut
|
|||
packages.published=Yayınlandı
|
||||
|
||||
defaulthooks=Varsayılan Web İstemcileri
|
||||
defaulthooks.desc=Web İstemcileri, belirli Gitea olayları tetiklendiğinde otomatik olarak HTTP POST isteklerini sunucuya yapar. Burada tanımlanan Web İstemcileri varsayılandır ve tüm yeni depolara kopyalanır. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> daha fazla bilgi edinin.
|
||||
defaulthooks.add_webhook=Varsayılan Web İstemcisi Ekle
|
||||
defaulthooks.update_webhook=Varsayılan Web İstemcisini Güncelle
|
||||
|
||||
systemhooks=Sistem Web İstemcileri
|
||||
systemhooks.desc=Belirli Gitea olayları tetiklendiğinde Web istemcileri otomatik olarak bir sunucuya HTTP POST istekleri yapar. Burada tanımlanan web istemcileri sistemdeki tüm depolar üzerinde çalışır, bu yüzden lütfen bunun olabilecek tüm performans sonuçlarını göz önünde bulundurun. <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/webhooks">web istemcileri kılavuzunda</a> daha fazla bilgi edinin.
|
||||
systemhooks.add_webhook=Sistem Web İstemcisi Ekle
|
||||
systemhooks.update_webhook=Sistem Web İstemcisi Güncelle
|
||||
|
||||
|
@ -2949,6 +3009,7 @@ auths.tip.google_plus=OAuth2 istemci kimlik bilgilerini https://console.develope
|
|||
auths.tip.openid_connect=Bitiş noktalarını belirlemek için OpenID Connect Discovery URL'sini kullanın (<server>/.well-known/openid-configuration)
|
||||
auths.tip.twitter=https://dev.twitter.com/apps adresine gidin, bir uygulama oluşturun ve “Bu uygulamanın Twitter ile oturum açmak için kullanılmasına izin ver” seçeneğinin etkin olduğundan emin olun
|
||||
auths.tip.discord=https://discordapp.com/developers/applications/me adresinde yeni bir uygulama kaydedin
|
||||
auths.tip.gitea=Yeni bir OAuth2 uygulaması kaydedin. Rehber https://docs.gitea.com/development/oauth2-provider adresinde bulunabilir
|
||||
auths.tip.yandex=`https://oauth.yandex.com/client/new adresinde yeni bir uygulama oluşturun. "Yandex.Passport API'sı" bölümünden aşağıdaki izinleri seçin: "E-posta adresine erişim", "Kullanıcı avatarına erişim" ve "Kullanıcı adına, ad ve soyadına, cinsiyete erişim"`
|
||||
auths.tip.mastodon=Kimlik doğrulaması yapmak istediğiniz mastodon örneği için özel bir örnek URL girin (veya varsayılan olanı kullanın)
|
||||
auths.edit=Kimlik Doğrulama Kaynağı Düzenle
|
||||
|
@ -3128,8 +3189,10 @@ monitor.queue.name=İsim
|
|||
monitor.queue.type=Tür
|
||||
monitor.queue.exemplar=Örnek Türü
|
||||
monitor.queue.numberworkers=Çalışan Sayısı
|
||||
monitor.queue.activeworkers=Etkin Çalışanlar
|
||||
monitor.queue.maxnumberworkers=En Fazla Çalışan Sayısı
|
||||
monitor.queue.numberinqueue=Kuyruktaki Sayı
|
||||
monitor.queue.review_add=Çalışanları İncele / Ekle
|
||||
monitor.queue.settings.title=Havuz Ayarları
|
||||
monitor.queue.settings.desc=Havuzlar, çalışan kuyruğu tıkanmasına bir yanıt olarak dinamik olarak büyürler.
|
||||
monitor.queue.settings.maxnumberworkers=En fazla çalışan Sayısı
|
||||
|
@ -3456,23 +3519,31 @@ runners.status.idle=Boşta
|
|||
runners.status.active=Etkin
|
||||
runners.status.offline=Çevrimdışı
|
||||
runners.version=Sürüm
|
||||
runners.reset_registration_token=Kayıt tokenini sıfırla
|
||||
runners.reset_registration_token_success=Çalıştırıcı kayıt belirteci başarıyla sıfırlandı
|
||||
|
||||
runs.all_workflows=Tüm İş Akışları
|
||||
runs.commit=İşle
|
||||
runs.scheduled=Zamanlanmış
|
||||
runs.pushed_by=iten
|
||||
runs.invalid_workflow_helper=İş akışı yapılandırma dosyası geçersiz. Lütfen yapılandırma dosyanızı denetleyin: %s
|
||||
runs.no_matching_online_runner_helper=Şu etiket ile eşleşen çevrimiçi çalıştırıcı bulunamadı: %s
|
||||
runs.actor=Aktör
|
||||
runs.status=Durum
|
||||
runs.actors_no_select=Tüm aktörler
|
||||
runs.status_no_select=Tüm durumlar
|
||||
runs.no_results=Eşleşen sonuç yok.
|
||||
runs.no_workflows=Henüz hiç bir iş akışı yok.
|
||||
runs.no_workflows.quick_start=Gitea İşlem'i nasıl başlatacağınızı bilmiyor musunuz? <a target="_blank" rel="noopener noreferrer" href="%s">Hızlı başlangıç rehberine</a> bakabilirsiniz.
|
||||
runs.no_workflows.documentation=Gitea İşlem'i hakkında daha fazla bilgi için, <a target="_blank" rel="noopener noreferrer" href="%s">belgeye</a> bakabilirsiniz.
|
||||
runs.no_runs=İş akışı henüz hiç çalıştırılmadı.
|
||||
runs.empty_commit_message=(boş işleme iletisi)
|
||||
|
||||
workflow.disable=İş Akışını Devre Dışı Bırak
|
||||
workflow.disable_success='%s' iş akışı başarıyla devre dışı bırakıldı.
|
||||
workflow.enable=İş Akışını Etkinleştir
|
||||
workflow.enable_success='%s' iş akışı başarıyla etkinleştirildi.
|
||||
workflow.disabled=İş akışı devre dışı.
|
||||
|
||||
need_approval_desc=Değişiklik isteği çatalında iş akışı çalıştırmak için onay gerekiyor.
|
||||
|
||||
|
|
1740
package-lock.json
generated
1740
package-lock.json
generated
File diff suppressed because it is too large
Load diff
21
package.json
21
package.json
|
@ -17,15 +17,20 @@
|
|||
"@webcomponents/custom-elements": "1.6.0",
|
||||
"add-asset-webpack-plugin": "2.0.1",
|
||||
"ansi_up": "6.0.2",
|
||||
"asciinema-player": "3.6.3",
|
||||
"asciinema-player": "3.6.4",
|
||||
"chart.js": "4.4.1",
|
||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"clippie": "4.0.6",
|
||||
"css-loader": "6.10.0",
|
||||
"dayjs": "1.11.10",
|
||||
"dropzone": "6.0.0-beta.2",
|
||||
"easymde": "2.18.0",
|
||||
"esbuild-loader": "4.0.3",
|
||||
"escape-goat": "4.0.0",
|
||||
"fast-glob": "3.3.2",
|
||||
"htmx.org": "1.9.10",
|
||||
"idiomorph": "0.3.0",
|
||||
"jquery": "3.7.1",
|
||||
"katex": "0.16.9",
|
||||
"license-checker-webpack-plugin": "0.2.1",
|
||||
|
@ -34,21 +39,22 @@
|
|||
"minimatch": "9.0.3",
|
||||
"monaco-editor": "0.46.0",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"pdfobject": "2.2.12",
|
||||
"pdfobject": "2.3.0",
|
||||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.2",
|
||||
"swagger-ui-dist": "5.11.3",
|
||||
"swagger-ui-dist": "5.11.6",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tippy.js": "6.3.7",
|
||||
"toastify-js": "1.12.0",
|
||||
"tributejs": "5.1.3",
|
||||
"uint8-to-base64": "0.2.0",
|
||||
"vue": "3.4.18",
|
||||
"vue": "3.4.19",
|
||||
"vue-bar-graph": "2.0.0",
|
||||
"vue-chartjs": "5.3.0",
|
||||
"vue-loader": "17.4.2",
|
||||
"vue3-calendar-heatmap": "2.0.5",
|
||||
"webpack": "5.90.1",
|
||||
"webpack": "5.90.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
"wrap-ansi": "9.0.0"
|
||||
},
|
||||
|
@ -56,17 +62,18 @@
|
|||
"@eslint-community/eslint-plugin-eslint-comments": "4.1.0",
|
||||
"@playwright/test": "1.41.2",
|
||||
"@stoplight/spectral-cli": "6.11.0",
|
||||
"@stylistic/eslint-plugin-js": "1.6.1",
|
||||
"@stylistic/eslint-plugin-js": "1.6.2",
|
||||
"@stylistic/stylelint-plugin": "2.0.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-plugin-array-func": "4.0.0",
|
||||
"eslint-plugin-github": "4.10.1",
|
||||
"eslint-plugin-i": "2.29.1",
|
||||
"eslint-plugin-jquery": "1.5.1",
|
||||
"eslint-plugin-no-jquery": "2.7.0",
|
||||
"eslint-plugin-no-use-extend-native": "0.5.0",
|
||||
"eslint-plugin-regexp": "2.2.0",
|
||||
"eslint-plugin-sonarjs": "0.23.0",
|
||||
"eslint-plugin-sonarjs": "0.24.0",
|
||||
"eslint-plugin-unicorn": "51.0.1",
|
||||
"eslint-plugin-vitest": "0.3.22",
|
||||
"eslint-plugin-vitest-globals": "1.4.0",
|
||||
|
|
8
poetry.lock
generated
8
poetry.lock
generated
|
@ -342,13 +342,13 @@ telegram = ["requests"]
|
|||
|
||||
[[package]]
|
||||
name = "yamllint"
|
||||
version = "1.34.0"
|
||||
version = "1.35.0"
|
||||
description = "A linter for YAML files."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "yamllint-1.34.0-py3-none-any.whl", hash = "sha256:33b813f6ff2ffad2e57a288281098392b85f7463ce1f3d5cd45aa848b916a806"},
|
||||
{file = "yamllint-1.34.0.tar.gz", hash = "sha256:7f0a6a41e8aab3904878da4ae34b6248b6bc74634e0d3a90f0fb2d7e723a3d4f"},
|
||||
{file = "yamllint-1.35.0-py3-none-any.whl", hash = "sha256:601b0adaaac6d9bacb16a2e612e7ee8d23caf941ceebf9bfe2cff0f196266004"},
|
||||
{file = "yamllint-1.35.0.tar.gz", hash = "sha256:9bc99c3e9fe89b4c6ee26e17aa817cf2d14390de6577cb6e2e6ed5f72120c835"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -361,4 +361,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "e4ea4301a70487379fce7008493d15c005af3aada7d88fbf0bd3167147ec6502"
|
||||
content-hash = "ba1c2c4235872f67354b5f52aa5bf0cd616354961530d9dc907f9fba28cc1ece"
|
||||
|
|
|
@ -9,7 +9,7 @@ python = "^3.8"
|
|||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
djlint = "1.34.1"
|
||||
yamllint = "1.34.0"
|
||||
yamllint = "1.35.0"
|
||||
|
||||
[tool.djlint]
|
||||
profile="golang"
|
||||
|
|
|
@ -63,6 +63,7 @@ package actions
|
|||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -426,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
|
|||
|
||||
var items []downloadArtifactResponseItem
|
||||
for _, artifact := range artifacts {
|
||||
downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
|
||||
var downloadURL string
|
||||
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
|
||||
u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
|
||||
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
|
||||
log.Error("Error getting serve direct url: %v", err)
|
||||
}
|
||||
if u != nil {
|
||||
downloadURL = u.String()
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
|
||||
}
|
||||
item := downloadArtifactResponseItem{
|
||||
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),
|
||||
ItemType: "file",
|
||||
|
|
|
@ -28,13 +28,14 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
tplDashboard base.TplName = "admin/dashboard"
|
||||
tplSelfCheck base.TplName = "admin/self_check"
|
||||
tplCron base.TplName = "admin/cron"
|
||||
tplQueue base.TplName = "admin/queue"
|
||||
tplStacktrace base.TplName = "admin/stacktrace"
|
||||
tplQueueManage base.TplName = "admin/queue_manage"
|
||||
tplStats base.TplName = "admin/stats"
|
||||
tplDashboard base.TplName = "admin/dashboard"
|
||||
tplSystemStatus base.TplName = "admin/system_status"
|
||||
tplSelfCheck base.TplName = "admin/self_check"
|
||||
tplCron base.TplName = "admin/cron"
|
||||
tplQueue base.TplName = "admin/queue"
|
||||
tplStacktrace base.TplName = "admin/stacktrace"
|
||||
tplQueueManage base.TplName = "admin/queue_manage"
|
||||
tplStats base.TplName = "admin/stats"
|
||||
)
|
||||
|
||||
var sysStatus struct {
|
||||
|
@ -72,7 +73,7 @@ var sysStatus struct {
|
|||
|
||||
// Garbage collector statistics.
|
||||
NextGC string // next run in HeapAlloc time (bytes)
|
||||
LastGC string // last run in absolute time (ns)
|
||||
LastGCTime string // last run time
|
||||
PauseTotalNs string
|
||||
PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256]
|
||||
NumGC uint32
|
||||
|
@ -110,7 +111,7 @@ func updateSystemStatus() {
|
|||
sysStatus.OtherSys = base.FileSize(int64(m.OtherSys))
|
||||
|
||||
sysStatus.NextGC = base.FileSize(int64(m.NextGC))
|
||||
sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000)
|
||||
sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339)
|
||||
sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000)
|
||||
sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000)
|
||||
sysStatus.NumGC = m.NumGC
|
||||
|
@ -132,7 +133,6 @@ func Dashboard(ctx *context.Context) {
|
|||
ctx.Data["PageIsAdminDashboard"] = true
|
||||
ctx.Data["NeedUpdate"] = updatechecker.GetNeedUpdate(ctx)
|
||||
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
|
||||
// FIXME: update periodically
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.Data["SSH"] = setting.SSH
|
||||
|
@ -140,6 +140,12 @@ func Dashboard(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplDashboard)
|
||||
}
|
||||
|
||||
func SystemStatus(ctx *context.Context) {
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.HTML(http.StatusOK, tplSystemStatus)
|
||||
}
|
||||
|
||||
// DashboardPost run an admin operation
|
||||
func DashboardPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
|
||||
|
|
|
@ -22,6 +22,8 @@ func Activity(ctx *context.Context) {
|
|||
ctx.Data["Title"] = ctx.Tr("repo.activity")
|
||||
ctx.Data["PageIsActivity"] = true
|
||||
|
||||
ctx.Data["PageIsPulse"] = true
|
||||
|
||||
ctx.Data["Period"] = ctx.Params("period")
|
||||
|
||||
timeUntil := time.Now()
|
||||
|
|
44
routers/web/repo/contributors.go
Normal file
44
routers/web/repo/contributors.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
contributors_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tplContributors base.TplName = "repo/activity"
|
||||
)
|
||||
|
||||
// Contributors render the page to show repository contributors graph
|
||||
func Contributors(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.contributors")
|
||||
|
||||
ctx.Data["PageIsActivity"] = true
|
||||
ctx.Data["PageIsContributors"] = true
|
||||
|
||||
ctx.PageData["contributionType"] = "commits"
|
||||
|
||||
ctx.PageData["repoLink"] = ctx.Repo.RepoLink
|
||||
|
||||
ctx.HTML(http.StatusOK, tplContributors)
|
||||
}
|
||||
|
||||
// ContributorsData renders JSON of contributors along with their weekly commit statistics
|
||||
func ContributorsData(ctx *context.Context) {
|
||||
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
|
||||
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
|
||||
ctx.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetContributorStats", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, contributorStats)
|
||||
}
|
||||
}
|
|
@ -151,6 +151,7 @@ func WebhooksNew(ctx *context.Context) {
|
|||
}
|
||||
}
|
||||
ctx.Data["BaseLink"] = orCtx.LinkNew
|
||||
ctx.Data["BaseLinkNew"] = orCtx.LinkNew
|
||||
|
||||
ctx.HTML(http.StatusOK, orCtx.NewTemplate)
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
|
||||
const (
|
||||
tplProfileBigAvatar base.TplName = "shared/user/profile_big_avatar"
|
||||
tplFollowUnfollow base.TplName = "org/follow_unfollow"
|
||||
)
|
||||
|
||||
// OwnerProfile render profile page for a user or a organization (aka, repo owner)
|
||||
|
@ -349,6 +350,15 @@ func Action(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
||||
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
|
||||
if ctx.ContextUser.IsIndividual() {
|
||||
shared_user.PrepareContextForProfileBigAvatar(ctx)
|
||||
ctx.HTML(http.StatusOK, tplProfileBigAvatar)
|
||||
return
|
||||
} else if ctx.ContextUser.IsOrganization() {
|
||||
ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
|
||||
ctx.HTML(http.StatusOK, tplFollowUnfollow)
|
||||
return
|
||||
}
|
||||
log.Error("Failed to apply action %q: unsupport context user type: %s", ctx.FormString("action"), ctx.ContextUser.Type)
|
||||
ctx.Error(http.StatusBadRequest, fmt.Sprintf("Action %q failed", ctx.FormString("action")))
|
||||
}
|
||||
|
|
|
@ -681,6 +681,7 @@ func registerRoutes(m *web.Route) {
|
|||
// ***** START: Admin *****
|
||||
m.Group("/admin", func() {
|
||||
m.Get("", admin.Dashboard)
|
||||
m.Get("/system_status", admin.SystemStatus)
|
||||
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
|
||||
|
||||
if setting.Database.Type.IsMySQL() || setting.Database.Type.IsMSSQL() {
|
||||
|
@ -1431,6 +1432,10 @@ func registerRoutes(m *web.Route) {
|
|||
m.Group("/activity", func() {
|
||||
m.Get("", repo.Activity)
|
||||
m.Get("/{period}", repo.Activity)
|
||||
m.Group("/contributors", func() {
|
||||
m.Get("", repo.Contributors)
|
||||
m.Get("/data", repo.ContributorsData)
|
||||
})
|
||||
}, context.RepoRef(), repo.MustBeNotEmpty, context.RequireRepoReaderOr(unit.TypePullRequests, unit.TypeIssues, unit.TypeReleases))
|
||||
|
||||
m.Group("/activity_author_data", func() {
|
||||
|
|
|
@ -342,7 +342,7 @@ func buildReleaseFiles(ctx context.Context, ownerID int64, repoVersion *packages
|
|||
fmt.Fprintf(w, "Components: %s\n", strings.Join(components, " "))
|
||||
fmt.Fprintf(w, "Architectures: %s\n", strings.Join(architectures, " "))
|
||||
fmt.Fprintf(w, "Date: %s\n", time.Now().UTC().Format(time.RFC1123))
|
||||
fmt.Fprint(w, "Acquire-By-Hash: yes")
|
||||
fmt.Fprint(w, "Acquire-By-Hash: yes\n")
|
||||
|
||||
pfds, err := packages_model.GetPackageFileDescriptors(ctx, pfs)
|
||||
if err != nil {
|
||||
|
|
319
services/repository/contributors_graph.go
Normal file
319
services/repository/contributors_graph.go
Normal file
|
@ -0,0 +1,319 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"gitea.com/go-chi/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
contributorStatsCacheKey = "GetContributorStats/%s/%s"
|
||||
contributorStatsCacheTimeout int64 = 60 * 10
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAwaitGeneration = errors.New("generation took longer than ")
|
||||
awaitGenerationTime = time.Second * 5
|
||||
generateLock = sync.Map{}
|
||||
)
|
||||
|
||||
type WeekData struct {
|
||||
Week int64 `json:"week"` // Starting day of the week as Unix timestamp
|
||||
Additions int `json:"additions"` // Number of additions in that week
|
||||
Deletions int `json:"deletions"` // Number of deletions in that week
|
||||
Commits int `json:"commits"` // Number of commits in that week
|
||||
}
|
||||
|
||||
// ContributorData represents statistical git commit count data
|
||||
type ContributorData struct {
|
||||
Name string `json:"name"` // Display name of the contributor
|
||||
Login string `json:"login"` // Login name of the contributor in case it exists
|
||||
AvatarLink string `json:"avatar_link"`
|
||||
HomeLink string `json:"home_link"`
|
||||
TotalCommits int64 `json:"total_commits"`
|
||||
Weeks map[int64]*WeekData `json:"weeks"`
|
||||
}
|
||||
|
||||
// ExtendedCommitStats contains information for commit stats with author data
|
||||
type ExtendedCommitStats struct {
|
||||
Author *api.CommitUser `json:"author"`
|
||||
Stats *api.CommitStats `json:"stats"`
|
||||
}
|
||||
|
||||
const layout = time.DateOnly
|
||||
|
||||
func findLastSundayBeforeDate(dateStr string) (string, error) {
|
||||
date, err := time.Parse(layout, dateStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
weekday := date.Weekday()
|
||||
daysToSubtract := int(weekday) - int(time.Sunday)
|
||||
if daysToSubtract < 0 {
|
||||
daysToSubtract += 7
|
||||
}
|
||||
|
||||
lastSunday := date.AddDate(0, 0, -daysToSubtract)
|
||||
return lastSunday.Format(layout), nil
|
||||
}
|
||||
|
||||
// GetContributorStats returns contributors stats for git commits for given revision or default branch
|
||||
func GetContributorStats(ctx context.Context, cache cache.Cache, repo *repo_model.Repository, revision string) (map[string]*ContributorData, error) {
|
||||
// as GetContributorStats is resource intensive we cache the result
|
||||
cacheKey := fmt.Sprintf(contributorStatsCacheKey, repo.FullName(), revision)
|
||||
if !cache.IsExist(cacheKey) {
|
||||
genReady := make(chan struct{})
|
||||
|
||||
// dont start multible async generations
|
||||
_, run := generateLock.Load(cacheKey)
|
||||
if run {
|
||||
return nil, ErrAwaitGeneration
|
||||
}
|
||||
|
||||
generateLock.Store(cacheKey, struct{}{})
|
||||
// run generation async
|
||||
go generateContributorStats(genReady, cache, cacheKey, repo, revision)
|
||||
|
||||
select {
|
||||
case <-time.After(awaitGenerationTime):
|
||||
return nil, ErrAwaitGeneration
|
||||
case <-genReady:
|
||||
// we got generation ready before timeout
|
||||
break
|
||||
}
|
||||
}
|
||||
// TODO: renew timeout of cache cache.UpdateTimeout(cacheKey, contributorStatsCacheTimeout)
|
||||
|
||||
switch v := cache.Get(cacheKey).(type) {
|
||||
case error:
|
||||
return nil, v
|
||||
case map[string]*ContributorData:
|
||||
return v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type in cache detected")
|
||||
}
|
||||
}
|
||||
|
||||
// getExtendedCommitStats return the list of *ExtendedCommitStats for the given revision
|
||||
func getExtendedCommitStats(repo *git.Repository, revision string /*, limit int */) ([]*ExtendedCommitStats, error) {
|
||||
baseCommit, err := repo.GetCommit(revision)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = stdoutReader.Close()
|
||||
_ = stdoutWriter.Close()
|
||||
}()
|
||||
|
||||
gitCmd := git.NewCommand(repo.Ctx, "log", "--shortstat", "--no-merges", "--pretty=format:---%n%aN%n%aE%n%as", "--reverse")
|
||||
// AddOptionFormat("--max-count=%d", limit)
|
||||
gitCmd.AddDynamicArguments(baseCommit.ID.String())
|
||||
|
||||
var extendedCommitStats []*ExtendedCommitStats
|
||||
stderr := new(strings.Builder)
|
||||
err = gitCmd.Run(&git.RunOpts{
|
||||
Dir: repo.Path,
|
||||
Stdout: stdoutWriter,
|
||||
Stderr: stderr,
|
||||
PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error {
|
||||
_ = stdoutWriter.Close()
|
||||
scanner := bufio.NewScanner(stdoutReader)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "---" {
|
||||
continue
|
||||
}
|
||||
scanner.Scan()
|
||||
authorName := strings.TrimSpace(scanner.Text())
|
||||
scanner.Scan()
|
||||
authorEmail := strings.TrimSpace(scanner.Text())
|
||||
scanner.Scan()
|
||||
date := strings.TrimSpace(scanner.Text())
|
||||
scanner.Scan()
|
||||
stats := strings.TrimSpace(scanner.Text())
|
||||
if authorName == "" || authorEmail == "" || date == "" || stats == "" {
|
||||
// FIXME: find a better way to parse the output so that we will handle this properly
|
||||
log.Warn("Something is wrong with git log output, skipping...")
|
||||
log.Warn("authorName: %s, authorEmail: %s, date: %s, stats: %s", authorName, authorEmail, date, stats)
|
||||
continue
|
||||
}
|
||||
// 1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
fields := strings.Split(stats, ",")
|
||||
|
||||
commitStats := api.CommitStats{}
|
||||
for _, field := range fields[1:] {
|
||||
parts := strings.Split(strings.TrimSpace(field), " ")
|
||||
value, contributionType := parts[0], parts[1]
|
||||
amount, _ := strconv.Atoi(value)
|
||||
|
||||
if strings.HasPrefix(contributionType, "insertion") {
|
||||
commitStats.Additions = amount
|
||||
} else {
|
||||
commitStats.Deletions = amount
|
||||
}
|
||||
}
|
||||
commitStats.Total = commitStats.Additions + commitStats.Deletions
|
||||
scanner.Scan()
|
||||
scanner.Text() // empty line at the end
|
||||
|
||||
res := &ExtendedCommitStats{
|
||||
Author: &api.CommitUser{
|
||||
Identity: api.Identity{
|
||||
Name: authorName,
|
||||
Email: authorEmail,
|
||||
},
|
||||
Date: date,
|
||||
},
|
||||
Stats: &commitStats,
|
||||
}
|
||||
extendedCommitStats = append(extendedCommitStats, res)
|
||||
|
||||
}
|
||||
_ = stdoutReader.Close()
|
||||
return nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to get ContributorsCommitStats for repository.\nError: %w\nStderr: %s", err, stderr)
|
||||
}
|
||||
|
||||
return extendedCommitStats, nil
|
||||
}
|
||||
|
||||
func generateContributorStats(genDone chan struct{}, cache cache.Cache, cacheKey string, repo *repo_model.Repository, revision string) {
|
||||
ctx := graceful.GetManager().HammerContext()
|
||||
|
||||
gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("OpenRepository: %w", err)
|
||||
_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
|
||||
return
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if len(revision) == 0 {
|
||||
revision = repo.DefaultBranch
|
||||
}
|
||||
extendedCommitStats, err := getExtendedCommitStats(gitRepo, revision)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("ExtendedCommitStats: %w", err)
|
||||
_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
|
||||
return
|
||||
}
|
||||
if len(extendedCommitStats) == 0 {
|
||||
err := fmt.Errorf("no commit stats returned for revision '%s'", revision)
|
||||
_ = cache.Put(cacheKey, err, contributorStatsCacheTimeout)
|
||||
return
|
||||
}
|
||||
|
||||
layout := time.DateOnly
|
||||
|
||||
unknownUserAvatarLink := user_model.NewGhostUser().AvatarLinkWithSize(ctx, 0)
|
||||
contributorsCommitStats := make(map[string]*ContributorData)
|
||||
contributorsCommitStats["total"] = &ContributorData{
|
||||
Name: "Total",
|
||||
Weeks: make(map[int64]*WeekData),
|
||||
}
|
||||
total := contributorsCommitStats["total"]
|
||||
|
||||
for _, v := range extendedCommitStats {
|
||||
userEmail := v.Author.Email
|
||||
if len(userEmail) == 0 {
|
||||
continue
|
||||
}
|
||||
u, _ := user_model.GetUserByEmail(ctx, userEmail)
|
||||
if u != nil {
|
||||
// update userEmail with user's primary email address so
|
||||
// that different mail addresses will linked to same account
|
||||
userEmail = u.GetEmail()
|
||||
}
|
||||
// duplicated logic
|
||||
if _, ok := contributorsCommitStats[userEmail]; !ok {
|
||||
if u == nil {
|
||||
avatarLink := avatars.GenerateEmailAvatarFastLink(ctx, userEmail, 0)
|
||||
if avatarLink == "" {
|
||||
avatarLink = unknownUserAvatarLink
|
||||
}
|
||||
contributorsCommitStats[userEmail] = &ContributorData{
|
||||
Name: v.Author.Name,
|
||||
AvatarLink: avatarLink,
|
||||
Weeks: make(map[int64]*WeekData),
|
||||
}
|
||||
} else {
|
||||
contributorsCommitStats[userEmail] = &ContributorData{
|
||||
Name: u.DisplayName(),
|
||||
Login: u.LowerName,
|
||||
AvatarLink: u.AvatarLinkWithSize(ctx, 0),
|
||||
HomeLink: u.HomeLink(),
|
||||
Weeks: make(map[int64]*WeekData),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update user statistics
|
||||
user := contributorsCommitStats[userEmail]
|
||||
startingOfWeek, _ := findLastSundayBeforeDate(v.Author.Date)
|
||||
|
||||
val, _ := time.Parse(layout, startingOfWeek)
|
||||
week := val.UnixMilli()
|
||||
|
||||
if user.Weeks[week] == nil {
|
||||
user.Weeks[week] = &WeekData{
|
||||
Additions: 0,
|
||||
Deletions: 0,
|
||||
Commits: 0,
|
||||
Week: week,
|
||||
}
|
||||
}
|
||||
if total.Weeks[week] == nil {
|
||||
total.Weeks[week] = &WeekData{
|
||||
Additions: 0,
|
||||
Deletions: 0,
|
||||
Commits: 0,
|
||||
Week: week,
|
||||
}
|
||||
}
|
||||
user.Weeks[week].Additions += v.Stats.Additions
|
||||
user.Weeks[week].Deletions += v.Stats.Deletions
|
||||
user.Weeks[week].Commits++
|
||||
user.TotalCommits++
|
||||
|
||||
// Update overall statistics
|
||||
total.Weeks[week].Additions += v.Stats.Additions
|
||||
total.Weeks[week].Deletions += v.Stats.Deletions
|
||||
total.Weeks[week].Commits++
|
||||
total.TotalCommits++
|
||||
}
|
||||
|
||||
_ = cache.Put(cacheKey, contributorsCommitStats, contributorStatsCacheTimeout)
|
||||
generateLock.Delete(cacheKey)
|
||||
if genDone != nil {
|
||||
genDone <- struct{}{}
|
||||
}
|
||||
}
|
87
services/repository/contributors_graph_test.go
Normal file
87
services/repository/contributors_graph_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
"gitea.com/go-chi/cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepository_ContributorsGraph(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
assert.NoError(t, repo.LoadOwner(db.DefaultContext))
|
||||
mockCache, err := cache.NewCacher(cache.Options{
|
||||
Adapter: "memory",
|
||||
Interval: 24 * 60,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
generateContributorStats(nil, mockCache, "key", repo, "404ref")
|
||||
err, isErr := mockCache.Get("key").(error)
|
||||
assert.True(t, isErr)
|
||||
assert.ErrorAs(t, err, &git.ErrNotExist{})
|
||||
|
||||
generateContributorStats(nil, mockCache, "key2", repo, "master")
|
||||
data, isData := mockCache.Get("key2").(map[string]*ContributorData)
|
||||
assert.True(t, isData)
|
||||
var keys []string
|
||||
for k := range data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
assert.EqualValues(t, []string{
|
||||
"ethantkoenig@gmail.com",
|
||||
"jimmy.praet@telenet.be",
|
||||
"jon@allspice.io",
|
||||
"total", // generated summary
|
||||
}, keys)
|
||||
|
||||
assert.EqualValues(t, &ContributorData{
|
||||
Name: "Ethan Koenig",
|
||||
AvatarLink: "https://secure.gravatar.com/avatar/b42fb195faa8c61b8d88abfefe30e9e3?d=identicon",
|
||||
TotalCommits: 1,
|
||||
Weeks: map[int64]*WeekData{
|
||||
1511654400000: {
|
||||
Week: 1511654400000, // sunday 2017-11-26
|
||||
Additions: 3,
|
||||
Deletions: 0,
|
||||
Commits: 1,
|
||||
},
|
||||
},
|
||||
}, data["ethantkoenig@gmail.com"])
|
||||
assert.EqualValues(t, &ContributorData{
|
||||
Name: "Total",
|
||||
AvatarLink: "",
|
||||
TotalCommits: 3,
|
||||
Weeks: map[int64]*WeekData{
|
||||
1511654400000: {
|
||||
Week: 1511654400000, // sunday 2017-11-26 (2017-11-26 20:31:18 -0800)
|
||||
Additions: 3,
|
||||
Deletions: 0,
|
||||
Commits: 1,
|
||||
},
|
||||
1607817600000: {
|
||||
Week: 1607817600000, // sunday 2020-12-13 (2020-12-15 15:23:11 -0500)
|
||||
Additions: 10,
|
||||
Deletions: 0,
|
||||
Commits: 1,
|
||||
},
|
||||
1624752000000: {
|
||||
Week: 1624752000000, // sunday 2021-06-27 (2021-06-29 21:54:09 +0200)
|
||||
Additions: 2,
|
||||
Deletions: 0,
|
||||
Commits: 1,
|
||||
},
|
||||
},
|
||||
}, data["total"])
|
||||
}
|
|
@ -75,69 +75,9 @@
|
|||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.dashboard.system_status"}}
|
||||
</h4>
|
||||
<div class="ui attached table segment">
|
||||
<dl class="admin-dl-horizontal">
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt>
|
||||
<dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt>
|
||||
<dd>{{.SysStatus.NumGoroutine}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
|
||||
<dd>{{.SysStatus.MemAllocated}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
|
||||
<dd>{{.SysStatus.MemTotal}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.MemSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
|
||||
<dd>{{.SysStatus.Lookups}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
|
||||
<dd>{{.SysStatus.MemMallocs}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt>
|
||||
<dd>{{.SysStatus.MemFrees}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
|
||||
<dd>{{.SysStatus.HeapAlloc}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.HeapSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
|
||||
<dd>{{.SysStatus.HeapIdle}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
|
||||
<dd>{{.SysStatus.HeapInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
|
||||
<dd>{{.SysStatus.HeapReleased}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
|
||||
<dd>{{.SysStatus.HeapObjects}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
|
||||
<dd>{{.SysStatus.StackInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.StackSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
|
||||
<dd>{{.SysStatus.MSpanInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.MSpanSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
|
||||
<dd>{{.SysStatus.MCacheInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.MCacheSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.BuckHashSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.GCSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.OtherSys}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
|
||||
<dd>{{.SysStatus.NextGC}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
|
||||
<dd>{{.SysStatus.LastGC}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>
|
||||
<dd>{{.SysStatus.PauseTotalNs}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt>
|
||||
<dd>{{.SysStatus.PauseNs}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt>
|
||||
<dd>{{.SysStatus.NumGC}}</dd>
|
||||
</dl>
|
||||
{{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
|
||||
<div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".divider" class="ui attached table segment">
|
||||
{{template "admin/system_status" .}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
|
|
62
templates/admin/system_status.tmpl
Normal file
62
templates/admin/system_status.tmpl
Normal file
|
@ -0,0 +1,62 @@
|
|||
<dl class="admin-dl-horizontal">
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}</dt>
|
||||
<dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}</dt>
|
||||
<dd>{{.SysStatus.NumGoroutine}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}</dt>
|
||||
<dd>{{.SysStatus.MemAllocated}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}</dt>
|
||||
<dd>{{.SysStatus.MemTotal}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.MemSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}</dt>
|
||||
<dd>{{.SysStatus.Lookups}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}</dt>
|
||||
<dd>{{.SysStatus.MemMallocs}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}</dt>
|
||||
<dd>{{.SysStatus.MemFrees}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}</dt>
|
||||
<dd>{{.SysStatus.HeapAlloc}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.HeapSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}</dt>
|
||||
<dd>{{.SysStatus.HeapIdle}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}</dt>
|
||||
<dd>{{.SysStatus.HeapInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}</dt>
|
||||
<dd>{{.SysStatus.HeapReleased}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}</dt>
|
||||
<dd>{{.SysStatus.HeapObjects}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}</dt>
|
||||
<dd>{{.SysStatus.StackInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.StackSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}</dt>
|
||||
<dd>{{.SysStatus.MSpanInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.MSpanSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}</dt>
|
||||
<dd>{{.SysStatus.MCacheInuse}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.MCacheSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.BuckHashSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.GCSys}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}</dt>
|
||||
<dd>{{.SysStatus.OtherSys}}</dd>
|
||||
<div class="divider"></div>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}</dt>
|
||||
<dd>{{.SysStatus.NextGC}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}</dt>
|
||||
<dd><relative-time format="duration" datetime="{{.SysStatus.LastGCTime}}">{{.SysStatus.LastGCTime}}</relative-time></dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}</dt>
|
||||
<dd>{{.SysStatus.PauseTotalNs}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}</dt>
|
||||
<dd>{{.SysStatus.PauseNs}}</dd>
|
||||
<dt>{{ctx.Locale.Tr "admin.dashboard.gc_times"}}</dt>
|
||||
<dd>{{.SysStatus.NumGC}}</dd>
|
||||
</dl>
|
|
@ -30,7 +30,7 @@
|
|||
{{template "base/head_style" .}}
|
||||
{{template "custom/header" .}}
|
||||
</head>
|
||||
<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-push-url="false">
|
||||
<body hx-headers='{"x-csrf-token": "{{.CsrfToken}}"}' hx-swap="outerHTML" hx-ext="morph" hx-push-url="false">
|
||||
{{ctx.DataRaceCheck $.Context}}
|
||||
{{template "custom/body_outer_pre" .}}
|
||||
|
||||
|
|
7
templates/org/follow_unfollow.tmpl
Normal file
7
templates/org/follow_unfollow.tmpl
Normal file
|
@ -0,0 +1,7 @@
|
|||
<button class="ui basic button gt-mr-0" hx-post="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
|
||||
{{if $.IsFollowing}}
|
||||
{{ctx.Locale.Tr "user.unfollow"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "user.follow"}}
|
||||
{{end}}
|
||||
</button>
|
|
@ -30,13 +30,9 @@
|
|||
{{svg "octicon-rss" 24}}
|
||||
</a>
|
||||
{{end}}
|
||||
<button class="link-action ui basic button gt-mr-0" data-url="{{.Org.HomeLink}}?action={{if $.IsFollowing}}unfollow{{else}}follow{{end}}">
|
||||
{{if $.IsFollowing}}
|
||||
{{ctx.Locale.Tr "user.unfollow"}}
|
||||
{{else}}
|
||||
{{ctx.Locale.Tr "user.follow"}}
|
||||
{{end}}
|
||||
</button>
|
||||
{{if .IsSigned}}
|
||||
{{template "org/follow_unfollow" .}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,235 +1,15 @@
|
|||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository commits">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<h2 class="ui header activity-header">
|
||||
<span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span>
|
||||
<!-- Period -->
|
||||
<div class="ui floating dropdown jump filter">
|
||||
<div class="ui basic compact button">
|
||||
{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
|
||||
<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
|
||||
<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>
|
||||
<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a>
|
||||
<a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a>
|
||||
<a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a>
|
||||
<a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="divider"></div>
|
||||
|
||||
{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
|
||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4>
|
||||
<div class="ui attached segment two column grid">
|
||||
{{if .Permission.CanRead $.UnitTypePullRequests}}
|
||||
<div class="column">
|
||||
{{if gt .Activity.ActivePRCount 0}}
|
||||
<div class="stats-table">
|
||||
<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
|
||||
<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="stats-table">
|
||||
<a class="table-cell tiny background light grey"></a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Permission.CanRead $.UnitTypeIssues}}
|
||||
<div class="column">
|
||||
{{if gt .Activity.ActiveIssueCount 0}}
|
||||
<div class="stats-table">
|
||||
<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
|
||||
<a href="#new-issues" class="table-cell tiny background green"></a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="stats-table">
|
||||
<a class="table-cell tiny background light grey"></a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="ui container flex-container">
|
||||
<div class="flex-container-nav">
|
||||
{{template "repo/navbar" .}}
|
||||
</div>
|
||||
<div class="ui attached segment horizontal segments">
|
||||
{{if .Permission.CanRead $.UnitTypePullRequests}}
|
||||
<a href="#merged-pull-requests" class="ui attached segment text center">
|
||||
<span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
|
||||
</a>
|
||||
<a href="#proposed-pull-requests" class="ui attached segment text center">
|
||||
<span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Permission.CanRead $.UnitTypeIssues}}
|
||||
<a href="#closed-issues" class="ui attached segment text center">
|
||||
<span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
|
||||
</a>
|
||||
<a href="#new-issues" class="ui attached segment text center">
|
||||
<span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
|
||||
</a>
|
||||
{{end}}
|
||||
<div class="flex-container-main">
|
||||
{{if .PageIsPulse}}{{template "repo/pulse" .}}{{end}}
|
||||
{{if .PageIsContributors}}{{template "repo/contributors" .}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.CanRead $.UnitTypeCode}}
|
||||
{{if eq .Activity.Code.CommitCountInAllBranches 0}}
|
||||
<div class="ui center aligned segment">
|
||||
<h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if gt .Activity.Code.CommitCountInAllBranches 0}}
|
||||
<div class="ui attached segment horizontal segments">
|
||||
<div class="ui attached segment text">
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong>
|
||||
{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong>
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong>
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong>
|
||||
{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
|
||||
<strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong>
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
|
||||
<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
|
||||
</div>
|
||||
<div class="ui attached segment">
|
||||
<div id="repo-activity-top-authors-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.PublishedReleaseCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="published-releases">
|
||||
{{svg "octicon-tag" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
|
||||
(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
|
||||
(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.PublishedReleases}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
|
||||
{{.TagName}}
|
||||
{{if not .IsTag}}
|
||||
<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{end}}
|
||||
{{TimeSinceUnix .CreatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.MergedPRCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="merged-pull-requests">
|
||||
{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
|
||||
(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
|
||||
(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.MergedPRs}}
|
||||
<p class="desc">
|
||||
<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .MergedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.OpenedPRCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests">
|
||||
{{svg "octicon-git-branch" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
|
||||
(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
|
||||
(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.OpenedPRs}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.ClosedIssueCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="closed-issues">
|
||||
{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
|
||||
(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
|
||||
(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.ClosedIssues}}
|
||||
<p class="desc">
|
||||
<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .ClosedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.OpenedIssueCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="new-issues">
|
||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
|
||||
(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
|
||||
(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.OpenedIssues}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .CreatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.UnresolvedIssueCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
|
||||
{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.UnresolvedIssues}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
|
||||
#{{.Index}}
|
||||
{{if .IsPull}}
|
||||
<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{else}}
|
||||
<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{end}}
|
||||
{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
|
||||
|
|
13
templates/repo/contributors.tmpl
Normal file
13
templates/repo/contributors.tmpl
Normal file
|
@ -0,0 +1,13 @@
|
|||
{{if .Permission.CanRead $.UnitTypeCode}}
|
||||
<div id="repo-contributors-chart"
|
||||
data-locale-filter-label="{{ctx.Locale.Tr "repo.contributors.contribution_type.filter_label"}}"
|
||||
data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}"
|
||||
data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}"
|
||||
data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}"
|
||||
data-locale-loading-title="{{ctx.Locale.Tr "repo.contributors.loading_title"}}"
|
||||
data-locale-loading-title-failed="{{ctx.Locale.Tr "repo.contributors.loading_title_failed"}}"
|
||||
data-locale-loading-info="{{ctx.Locale.Tr "repo.contributors.loading_info"}}"
|
||||
data-locale-component-failed-to-load="{{ctx.Locale.Tr "repo.contributors.component_failed_to_load"}}"
|
||||
>
|
||||
</div>
|
||||
{{end}}
|
|
@ -5,9 +5,9 @@
|
|||
<div class="flex-item gt-ac">
|
||||
<div class="flex-item-leading">{{template "repo/icon" .}}</div>
|
||||
<div class="flex-item-main">
|
||||
<div class="flex-item-title">
|
||||
<a class="text light thin" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
|
||||
<a class="text primary" href="{{$.RepoLink}}">{{.Name}}</a></div>
|
||||
<div class="flex-item-title gt-font-18">
|
||||
<a class="muted gt-font-normal" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/
|
||||
<a class="muted" href="{{$.RepoLink}}">{{.Name}}</a></div>
|
||||
</div>
|
||||
<div class="flex-item-trailing">
|
||||
{{if .IsArchived}}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{{$avatarLink := (.RelAvatarLink ctx)}}
|
||||
{{if $avatarLink}}
|
||||
<img class="ui avatar gt-vm" src="{{$avatarLink}}" width="32" height="32" alt="{{.FullName}}">
|
||||
<img class="ui avatar gt-vm" src="{{$avatarLink}}" width="24" height="24" alt="{{.FullName}}">
|
||||
{{else if $.IsMirror}}
|
||||
{{svg "octicon-mirror" 32}}
|
||||
{{svg "octicon-mirror" 24}}
|
||||
{{else if $.IsFork}}
|
||||
{{svg "octicon-repo-forked" 32}}
|
||||
{{svg "octicon-repo-forked" 24}}
|
||||
{{else}}
|
||||
{{svg "octicon-repo" 32}}
|
||||
{{svg "octicon-repo" 24}}
|
||||
{{end}}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{template "repo/issue/branch_selector_field" .}}
|
||||
{{if .Issue.IsPull}}
|
||||
<input id="reviewer_id" name="reviewer_id" type="hidden" value="{{.reviewer_id}}">
|
||||
<div class="ui {{if or (not .Reviewers) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
|
||||
<div class="ui {{if or (and (not .Reviewers) (not .TeamReviewers)) (not .CanChooseReviewer) .Repository.IsArchived}}disabled{{end}} floating jump select-reviewers-modify dropdown">
|
||||
<a class="text gt-df gt-ac muted">
|
||||
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong>
|
||||
{{if and .CanChooseReviewer (not .Repository.IsArchived)}}
|
||||
|
@ -29,7 +29,9 @@
|
|||
{{end}}
|
||||
{{end}}
|
||||
{{if .TeamReviewers}}
|
||||
<div class="divider"></div>
|
||||
{{if .Reviewers}}
|
||||
<div class="divider"></div>
|
||||
{{end}}
|
||||
{{range .TeamReviewers}}
|
||||
{{if .Team}}
|
||||
<a class="{{if not .CanChange}}ui{{end}} item {{if .Checked}}checked{{end}} {{if not .CanChange}}ban-change{{end}}" href="#" data-id="{{.ItemID}}" data-id-selector="#review_request_team_{{.Team.ID}}" {{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
|
||||
|
|
8
templates/repo/navbar.tmpl
Normal file
8
templates/repo/navbar.tmpl
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class="ui fluid vertical menu">
|
||||
<a class="{{if .PageIsPulse}}active {{end}}item" href="{{.RepoLink}}/activity">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.pulse"}}
|
||||
</a>
|
||||
<a class="{{if .PageIsContributors}}active {{end}}item" href="{{.RepoLink}}/activity/contributors">
|
||||
{{ctx.Locale.Tr "repo.activity.navbar.contributors"}}
|
||||
</a>
|
||||
</div>
|
227
templates/repo/pulse.tmpl
Normal file
227
templates/repo/pulse.tmpl
Normal file
|
@ -0,0 +1,227 @@
|
|||
<h2 class="ui header activity-header">
|
||||
<span>{{DateTime "long" .DateFrom}} - {{DateTime "long" .DateUntil}}</span>
|
||||
<!-- Period -->
|
||||
<div class="ui floating dropdown jump filter">
|
||||
<div class="ui basic compact button">
|
||||
{{ctx.Locale.Tr "repo.activity.period.filter_label"}} <strong>{{.PeriodText}}</strong>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
</div>
|
||||
<div class="menu">
|
||||
<a class="{{if eq .Period "daily"}}active {{end}}item" href="{{$.RepoLink}}/activity/daily">{{ctx.Locale.Tr "repo.activity.period.daily"}}</a>
|
||||
<a class="{{if eq .Period "halfweekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/halfweekly">{{ctx.Locale.Tr "repo.activity.period.halfweekly"}}</a>
|
||||
<a class="{{if eq .Period "weekly"}}active {{end}}item" href="{{$.RepoLink}}/activity/weekly">{{ctx.Locale.Tr "repo.activity.period.weekly"}}</a>
|
||||
<a class="{{if eq .Period "monthly"}}active {{end}}item" href="{{$.RepoLink}}/activity/monthly">{{ctx.Locale.Tr "repo.activity.period.monthly"}}</a>
|
||||
<a class="{{if eq .Period "quarterly"}}active {{end}}item" href="{{$.RepoLink}}/activity/quarterly">{{ctx.Locale.Tr "repo.activity.period.quarterly"}}</a>
|
||||
<a class="{{if eq .Period "semiyearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/semiyearly">{{ctx.Locale.Tr "repo.activity.period.semiyearly"}}</a>
|
||||
<a class="{{if eq .Period "yearly"}}active {{end}}item" href="{{$.RepoLink}}/activity/yearly">{{ctx.Locale.Tr "repo.activity.period.yearly"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
{{if (or (.Permission.CanRead $.UnitTypeIssues) (.Permission.CanRead $.UnitTypePullRequests))}}
|
||||
<h4 class="ui top attached header">{{ctx.Locale.Tr "repo.activity.overview"}}</h4>
|
||||
<div class="ui attached segment two column grid">
|
||||
{{if .Permission.CanRead $.UnitTypePullRequests}}
|
||||
<div class="column">
|
||||
{{if gt .Activity.ActivePRCount 0}}
|
||||
<div class="stats-table">
|
||||
<a href="#merged-pull-requests" class="table-cell tiny background purple" style="width: {{.Activity.MergedPRPerc}}{{if ne .Activity.MergedPRPerc 0}}%{{end}}"></a>
|
||||
<a href="#proposed-pull-requests" class="table-cell tiny background green"></a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="stats-table">
|
||||
<a class="table-cell tiny background light grey"></a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ctx.Locale.TrN .Activity.ActivePRCount "repo.activity.active_prs_count_1" "repo.activity.active_prs_count_n" .Activity.ActivePRCount | Safe}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Permission.CanRead $.UnitTypeIssues}}
|
||||
<div class="column">
|
||||
{{if gt .Activity.ActiveIssueCount 0}}
|
||||
<div class="stats-table">
|
||||
<a href="#closed-issues" class="table-cell tiny background red" style="width: {{.Activity.ClosedIssuePerc}}{{if ne .Activity.ClosedIssuePerc 0}}%{{end}}"></a>
|
||||
<a href="#new-issues" class="table-cell tiny background green"></a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="stats-table">
|
||||
<a class="table-cell tiny background light grey"></a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ctx.Locale.TrN .Activity.ActiveIssueCount "repo.activity.active_issues_count_1" "repo.activity.active_issues_count_n" .Activity.ActiveIssueCount | Safe}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="ui attached segment horizontal segments">
|
||||
{{if .Permission.CanRead $.UnitTypePullRequests}}
|
||||
<a href="#merged-pull-requests" class="ui attached segment text center">
|
||||
<span class="text purple">{{svg "octicon-git-pull-request"}}</span> <strong>{{.Activity.MergedPRCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.merged_prs_count_1" "repo.activity.merged_prs_count_n"}}
|
||||
</a>
|
||||
<a href="#proposed-pull-requests" class="ui attached segment text center">
|
||||
<span class="text green">{{svg "octicon-git-branch"}}</span> <strong>{{.Activity.OpenedPRCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.opened_prs_count_1" "repo.activity.opened_prs_count_n"}}
|
||||
</a>
|
||||
{{end}}
|
||||
{{if .Permission.CanRead $.UnitTypeIssues}}
|
||||
<a href="#closed-issues" class="ui attached segment text center">
|
||||
<span class="text red">{{svg "octicon-issue-closed"}}</span> <strong>{{.Activity.ClosedIssueCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.closed_issues_count_1" "repo.activity.closed_issues_count_n"}}
|
||||
</a>
|
||||
<a href="#new-issues" class="ui attached segment text center">
|
||||
<span class="text green">{{svg "octicon-issue-opened"}}</span> <strong>{{.Activity.OpenedIssueCount}}</strong><br>
|
||||
{{ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.new_issues_count_1" "repo.activity.new_issues_count_n"}}
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.CanRead $.UnitTypeCode}}
|
||||
{{if eq .Activity.Code.CommitCountInAllBranches 0}}
|
||||
<div class="ui center aligned segment">
|
||||
<h4 class="ui header">{{ctx.Locale.Tr "repo.activity.no_git_activity"}}</h4>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if gt .Activity.Code.CommitCountInAllBranches 0}}
|
||||
<div class="ui attached segment horizontal segments">
|
||||
<div class="ui attached segment text">
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_exclude_merges"}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_author_1" "repo.activity.git_stats_author_n" .Activity.Code.AuthorCount}}</strong>
|
||||
{{ctx.Locale.TrN .Activity.Code.AuthorCount "repo.activity.git_stats_pushed_1" "repo.activity.git_stats_pushed_n"}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.CommitCount "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCount}}</strong>
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_push_to_branch" .Repository.DefaultBranch}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.CommitCountInAllBranches "repo.activity.git_stats_commit_1" "repo.activity.git_stats_commit_n" .Activity.Code.CommitCountInAllBranches}}</strong>
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_push_to_all_branches"}}
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_on_default_branch" .Repository.DefaultBranch}}
|
||||
<strong>{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_file_1" "repo.activity.git_stats_file_n" .Activity.Code.ChangedFiles}}</strong>
|
||||
{{ctx.Locale.TrN .Activity.Code.ChangedFiles "repo.activity.git_stats_files_changed_1" "repo.activity.git_stats_files_changed_n"}}
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_additions"}}
|
||||
<strong class="text green">{{ctx.Locale.TrN .Activity.Code.Additions "repo.activity.git_stats_addition_1" "repo.activity.git_stats_addition_n" .Activity.Code.Additions}}</strong>
|
||||
{{ctx.Locale.Tr "repo.activity.git_stats_and_deletions"}}
|
||||
<strong class="text red">{{ctx.Locale.TrN .Activity.Code.Deletions "repo.activity.git_stats_deletion_1" "repo.activity.git_stats_deletion_n" .Activity.Code.Deletions}}</strong>.
|
||||
</div>
|
||||
<div class="ui attached segment">
|
||||
<div id="repo-activity-top-authors-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.PublishedReleaseCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="published-releases">
|
||||
{{svg "octicon-tag" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.releases_published_by"
|
||||
(ctx.Locale.TrN .Activity.PublishedReleaseCount "repo.activity.title.releases_1" "repo.activity.title.releases_n" .Activity.PublishedReleaseCount)
|
||||
(ctx.Locale.TrN .Activity.PublishedReleaseAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.PublishedReleaseAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.PublishedReleases}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.published_release_label"}}</span>
|
||||
{{.TagName}}
|
||||
{{if not .IsTag}}
|
||||
<a class="title" href="{{$.RepoLink}}/src/{{.TagName | PathEscapeSegments}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{end}}
|
||||
{{TimeSinceUnix .CreatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.MergedPRCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="merged-pull-requests">
|
||||
{{svg "octicon-git-pull-request" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.prs_merged_by"
|
||||
(ctx.Locale.TrN .Activity.MergedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.MergedPRCount)
|
||||
(ctx.Locale.TrN .Activity.MergedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.MergedPRAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.MergedPRs}}
|
||||
<p class="desc">
|
||||
<span class="ui purple label">{{ctx.Locale.Tr "repo.activity.merged_prs_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .MergedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.OpenedPRCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="proposed-pull-requests">
|
||||
{{svg "octicon-git-branch" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.prs_opened_by"
|
||||
(ctx.Locale.TrN .Activity.OpenedPRCount "repo.activity.title.prs_1" "repo.activity.title.prs_n" .Activity.OpenedPRCount)
|
||||
(ctx.Locale.TrN .Activity.OpenedPRAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedPRAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.OpenedPRs}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.opened_prs_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Issue.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .Issue.CreatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.ClosedIssueCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="closed-issues">
|
||||
{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.issues_closed_from"
|
||||
(ctx.Locale.TrN .Activity.ClosedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.ClosedIssueCount)
|
||||
(ctx.Locale.TrN .Activity.ClosedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.ClosedIssueAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.ClosedIssues}}
|
||||
<p class="desc">
|
||||
<span class="ui red label">{{ctx.Locale.Tr "repo.activity.closed_issue_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .ClosedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.OpenedIssueCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="new-issues">
|
||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.Tr "repo.activity.title.issues_created_by"
|
||||
(ctx.Locale.TrN .Activity.OpenedIssueCount "repo.activity.title.issues_1" "repo.activity.title.issues_n" .Activity.OpenedIssueCount)
|
||||
(ctx.Locale.TrN .Activity.OpenedIssueAuthorCount "repo.activity.title.user_1" "repo.activity.title.user_n" .Activity.OpenedIssueAuthorCount)
|
||||
}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.OpenedIssues}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.new_issue_label"}}</span>
|
||||
#{{.Index}} <a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{TimeSinceUnix .CreatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if gt .Activity.UnresolvedIssueCount 0}}
|
||||
<h4 class="divider divider-text gt-normal-case" id="unresolved-conversations" data-tooltip-content="{{ctx.Locale.Tr "repo.activity.unresolved_conv_desc"}}">
|
||||
{{svg "octicon-comment-discussion" 16 "gt-mr-3"}}
|
||||
{{ctx.Locale.TrN .Activity.UnresolvedIssueCount "repo.activity.title.unresolved_conv_1" "repo.activity.title.unresolved_conv_n" .Activity.UnresolvedIssueCount}}
|
||||
</h4>
|
||||
<div class="list">
|
||||
{{range .Activity.UnresolvedIssues}}
|
||||
<p class="desc">
|
||||
<span class="ui green label">{{ctx.Locale.Tr "repo.activity.unresolved_conv_label"}}</span>
|
||||
#{{.Index}}
|
||||
{{if .IsPull}}
|
||||
<a class="title" href="{{$.RepoLink}}/pulls/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{else}}
|
||||
<a class="title" href="{{$.RepoLink}}/issues/{{.Index}}">{{.Title | RenderEmoji $.Context | RenderCodeBlock}}</a>
|
||||
{{end}}
|
||||
{{TimeSinceUnix .UpdatedUnix ctx.Locale}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
|
@ -18,11 +18,11 @@
|
|||
{{ctx.Locale.Tr "repo.settings.deploy_key_desc"}}
|
||||
</div>
|
||||
<div class="field {{if .Err_Title}}error{{end}}">
|
||||
<label for="title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
|
||||
<label for="ssh-key-title">{{ctx.Locale.Tr "repo.settings.title"}}</label>
|
||||
<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required>
|
||||
</div>
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
|
||||
<label for="ssh-key-content">{{ctx.Locale.Tr "repo.settings.deploy_key_content"}}</label>
|
||||
<textarea id="ssh-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
|
|
|
@ -3,56 +3,7 @@
|
|||
<div class="ui right">
|
||||
<div class="ui jump dropdown">
|
||||
<div class="ui primary tiny button">{{ctx.Locale.Tr "repo.settings.add_webhook"}}</div>
|
||||
<div class="menu">
|
||||
<a class="item" href="{{.BaseLinkNew}}/forgejo/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "forgejo" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_forgejo"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/gitea/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/gogs/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/slack/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "slack" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/discord/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "discord" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/dingtalk/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/telegram/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/msteams/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/feishu/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/matrix/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/wechatwork/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/packagist/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" 20)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}}
|
||||
</a>
|
||||
</div>
|
||||
{{template "repo/settings/webhook/link_menu" .}}
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
|
|
54
templates/repo/settings/webhook/link_menu.tmpl
Normal file
54
templates/repo/settings/webhook/link_menu.tmpl
Normal file
|
@ -0,0 +1,54 @@
|
|||
{{$size := 20}}
|
||||
{{if .Size}}
|
||||
{{$size = .Size}}
|
||||
{{end}}
|
||||
<div class="menu">
|
||||
<a class="item" href="{{.BaseLinkNew}}/forgejo/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "forgejo" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_forgejo"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/gitea/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "gitea" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_gitea"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/gogs/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "gogs" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_gogs"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/slack/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "slack" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_slack"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/discord/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "discord" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_discord"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/dingtalk/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "dingtalk" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_dingtalk"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/telegram/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "telegram" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_telegram"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/msteams/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "msteams" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_msteams"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/feishu/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "feishu" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/matrix/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "matrix" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_matrix"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/wechatwork/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "wechatwork" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_wechatwork"}}
|
||||
</a>
|
||||
<a class="item" href="{{.BaseLinkNew}}/packagist/new">
|
||||
{{template "shared/webhook/icon" (dict "HookType" "packagist" "Size" $size)}}
|
||||
{{ctx.Locale.Tr "repo.settings.web_hook_name_packagist"}}
|
||||
</a>
|
||||
</div>
|
|
@ -254,7 +254,7 @@
|
|||
<!-- Branch filter -->
|
||||
<div class="field">
|
||||
<label for="branch_filter">{{ctx.Locale.Tr "repo.settings.branch_filter"}}</label>
|
||||
<input name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
|
||||
<input id="branch_filter" name="branch_filter" type="text" value="{{or .Webhook.BranchFilter "*"}}">
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" | Str2html}}</span>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{{if eq .HookType "forgejo"}}
|
||||
<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/forgejo.svg">
|
||||
{{else if eq .HookType "gitea"}}
|
||||
<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gitea-original.svg">
|
||||
{{svg "gitea-gitea" $size "img"}}
|
||||
{{else if eq .HookType "gogs"}}
|
||||
<img width="{{$size}}" height="{{$size}}" src="{{AssetUrlPrefix}}/img/gogs.ico">
|
||||
{{else if eq .HookType "slack"}}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
{{.CsrfTokenHtml}}
|
||||
<input type="hidden" name="title" value="none">
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label>
|
||||
<label for="gpg-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
|
||||
<textarea id="gpg-key-content" name="content" placeholder="{{ctx.Locale.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
|
||||
</div>
|
||||
{{if .Err_Signature}}
|
||||
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
|
||||
<label for="gpg-key-signature">{{ctx.Locale.Tr "settings.gpg_token_signature"}}</label>
|
||||
<textarea id="gpg-key-signature" name="signature" placeholder="{{ctx.Locale.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{ctx.Locale.Tr "settings.principal_content"}}</label>
|
||||
<label for="ssh-principal-content">{{ctx.Locale.Tr "settings.principal_content"}}</label>
|
||||
<input id="ssh-principal-content" name="content" value="{{.content}}" autofocus required>
|
||||
</div>
|
||||
<input name="title" type="hidden" value="principal">
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="field {{if .Err_Title}}error{{end}}">
|
||||
<label for="title">{{ctx.Locale.Tr "settings.key_name"}}</label>
|
||||
<label for="ssh-key-title">{{ctx.Locale.Tr "settings.key_name"}}</label>
|
||||
<input id="ssh-key-title" name="title" value="{{.title}}" autofocus required maxlength="50">
|
||||
</div>
|
||||
<div class="field {{if .Err_Content}}error{{end}}">
|
||||
<label for="content">{{ctx.Locale.Tr "settings.key_content"}}</label>
|
||||
<label for="ssh-key-content">{{ctx.Locale.Tr "settings.key_content"}}</label>
|
||||
<textarea id="ssh-key-content" name="content" class="js-quick-submit" placeholder="{{ctx.Locale.Tr "settings.key_content_ssh_placeholder"}}" required>{{.content}}</textarea>
|
||||
</div>
|
||||
<input name="type" type="hidden" value="ssh">
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
<input id="full_name" name="full_name" value="{{.SignedUser.FullName}}" maxlength="100">
|
||||
</div>
|
||||
<div class="field {{if .Err_Email}}error{{end}}">
|
||||
<label for="email">{{ctx.Locale.Tr "email"}}</label>
|
||||
<p>{{.SignedUser.Email}}</p>
|
||||
<label>{{ctx.Locale.Tr "email"}}</label>
|
||||
<p id="signed-user-email">{{.SignedUser.Email}}</p>
|
||||
</div>
|
||||
<div class="field {{if .Err_Description}}error{{end}}">
|
||||
<label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label>
|
||||
|
@ -42,11 +42,11 @@
|
|||
<!-- private block -->
|
||||
|
||||
<div class="field" id="privacy-user-settings">
|
||||
<label for="security-private"><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label>
|
||||
<label><strong>{{ctx.Locale.Tr "settings.privacy"}}</strong></label>
|
||||
</div>
|
||||
|
||||
<div class="inline field {{if .Err_Visibility}}error{{end}}">
|
||||
<span class="inline required field"><label for="visibility">{{ctx.Locale.Tr "settings.visibility"}}</label></span>
|
||||
<span class="inline required field"><label>{{ctx.Locale.Tr "settings.visibility"}}</label></span>
|
||||
<div class="ui selection type dropdown">
|
||||
{{if .SignedUser.Visibility.IsPublic}}<input type="hidden" id="visibility" name="visibility" value="0">{{end}}
|
||||
{{if .SignedUser.Visibility.IsLimited}}<input type="hidden" id="visibility" name="visibility" value="1">{{end}}
|
||||
|
@ -120,8 +120,8 @@
|
|||
</div>
|
||||
|
||||
<div class="inline field gt-pl-4">
|
||||
<label for="avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
|
||||
<input name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
|
||||
<label for="new-avatar">{{ctx.Locale.Tr "settings.choose_new_avatar"}}</label>
|
||||
<input id="new-avatar" name="avatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
<h4 class="ui top attached header">
|
||||
{{.CustomHeaderTitle}}
|
||||
<div class="ui right">
|
||||
{{template "shared/webhook/icon" .ctxData}}
|
||||
<div class="ui right type dropdown">
|
||||
<div class="text gt-df gt-ac">
|
||||
{{template "shared/webhook/icon" (dict "Size" 20 "HookType" .ctxData.HookType)}}
|
||||
{{ctx.Locale.Tr (print "repo.settings.web_hook_name_" .ctxData.HookType)}}
|
||||
</div>
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
{{template "repo/settings/webhook/link_menu" .ctxData}}
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
|
|
|
@ -179,7 +179,7 @@ func TestLDAPUserSignin(t *testing.T) {
|
|||
|
||||
assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
|
||||
assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
|
||||
assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
|
||||
assert.Equal(t, u.Email, htmlDoc.Find("#signed-user-email").Text())
|
||||
}
|
||||
|
||||
func TestLDAPAuthChange(t *testing.T) {
|
||||
|
|
|
@ -413,6 +413,13 @@ ol.ui.list li,
|
|||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
/* extend fomantic style '.ui.dropdown > .text > img' to include svg.img */
|
||||
.ui.dropdown > .text > .img {
|
||||
margin-left: 0;
|
||||
float: none;
|
||||
margin-right: 0.78571429rem;
|
||||
}
|
||||
|
||||
.ui.dropdown > .text > .description,
|
||||
.ui.dropdown .menu > .item > .description {
|
||||
color: var(--color-text-light-2);
|
||||
|
|
|
@ -7,6 +7,10 @@ extends:
|
|||
- plugin:vue/vue3-recommended
|
||||
- plugin:vue-scoped-css/vue3-recommended
|
||||
|
||||
parserOptions:
|
||||
sourceType: module
|
||||
ecmaVersion: latest
|
||||
|
||||
env:
|
||||
browser: true
|
||||
|
||||
|
|
443
web_src/js/components/RepoContributors.vue
Normal file
443
web_src/js/components/RepoContributors.vue
Normal file
|
@ -0,0 +1,443 @@
|
|||
<script>
|
||||
import {SvgIcon} from '../svg.js';
|
||||
import {
|
||||
Chart,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
} from 'chart.js';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
||||
import {Line as ChartLine} from 'vue-chartjs';
|
||||
import {
|
||||
startDaysBetween,
|
||||
firstStartDateAfterDate,
|
||||
fillEmptyStartDaysWithZeroes,
|
||||
} from '../utils/time.js';
|
||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
||||
import $ from 'jquery';
|
||||
|
||||
const {pageData} = window.config;
|
||||
|
||||
const colors = {
|
||||
text: '--color-text',
|
||||
border: '--color-secondary-alpha-60',
|
||||
commits: '--color-primary-alpha-60',
|
||||
additions: '--color-green',
|
||||
deletions: '--color-red',
|
||||
title: '--color-secondary-dark-4',
|
||||
};
|
||||
|
||||
const styles = window.getComputedStyle(document.documentElement);
|
||||
const getColor = (name) => styles.getPropertyValue(name).trim();
|
||||
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
colors[key] = getColor(value);
|
||||
}
|
||||
|
||||
const customEventListener = {
|
||||
id: 'customEventListener',
|
||||
afterEvent: (chart, args, opts) => {
|
||||
// event will be replayed from chart.update when reset zoom,
|
||||
// so we need to check whether args.replay is true to avoid call loops
|
||||
if (args.event.type === 'dblclick' && opts.chartType === 'main' && !args.replay) {
|
||||
chart.resetZoom();
|
||||
opts.instance.updateOtherCharts(args.event, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Chart.defaults.color = colors.text;
|
||||
Chart.defaults.borderColor = colors.border;
|
||||
|
||||
Chart.register(
|
||||
TimeScale,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
BarElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
zoomPlugin,
|
||||
customEventListener,
|
||||
);
|
||||
|
||||
export default {
|
||||
components: {ChartLine, SvgIcon},
|
||||
props: {
|
||||
locale: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
errorText: '',
|
||||
totalStats: {},
|
||||
sortedContributors: {},
|
||||
repoLink: pageData.repoLink || [],
|
||||
type: pageData.contributionType,
|
||||
contributorsStats: [],
|
||||
xAxisStart: null,
|
||||
xAxisEnd: null,
|
||||
xAxisMin: null,
|
||||
xAxisMax: null,
|
||||
}),
|
||||
mounted() {
|
||||
this.fetchGraphData();
|
||||
|
||||
$('#repo-contributors').dropdown({
|
||||
onChange: (val) => {
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
this.type = val;
|
||||
this.sortContributors();
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
sortContributors() {
|
||||
const contributors = this.filterContributorWeeksByDateRange();
|
||||
const criteria = `total_${this.type}`;
|
||||
this.sortedContributors = Object.values(contributors)
|
||||
.filter((contributor) => contributor[criteria] !== 0)
|
||||
.sort((a, b) => a[criteria] > b[criteria] ? -1 : a[criteria] === b[criteria] ? 0 : 1)
|
||||
.slice(0, 100);
|
||||
},
|
||||
|
||||
async fetchGraphData() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
let response;
|
||||
do {
|
||||
response = await GET(`${this.repoLink}/activity/contributors/data`);
|
||||
if (response.status === 202) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying
|
||||
}
|
||||
} while (response.status === 202);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const {total, ...rest} = data;
|
||||
// below line might be deleted if we are sure go produces map always sorted by keys
|
||||
total.weeks = Object.fromEntries(Object.entries(total.weeks).sort());
|
||||
|
||||
const weekValues = Object.values(total.weeks);
|
||||
this.xAxisStart = weekValues[0].week;
|
||||
this.xAxisEnd = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(this.xAxisStart), new Date(this.xAxisEnd));
|
||||
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
this.contributorsStats = {};
|
||||
for (const [email, user] of Object.entries(rest)) {
|
||||
user.weeks = fillEmptyStartDaysWithZeroes(startDays, user.weeks);
|
||||
this.contributorsStats[email] = user;
|
||||
}
|
||||
this.sortContributors();
|
||||
this.totalStats = total;
|
||||
this.errorText = '';
|
||||
} else {
|
||||
this.errorText = response.statusText;
|
||||
}
|
||||
} catch (err) {
|
||||
this.errorText = err.message;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
filterContributorWeeksByDateRange() {
|
||||
const filteredData = {};
|
||||
const data = this.contributorsStats;
|
||||
for (const key of Object.keys(data)) {
|
||||
const user = data[key];
|
||||
user.total_commits = 0;
|
||||
user.total_additions = 0;
|
||||
user.total_deletions = 0;
|
||||
user.max_contribution_type = 0;
|
||||
const filteredWeeks = user.weeks.filter((week) => {
|
||||
const oneWeek = 7 * 24 * 60 * 60 * 1000;
|
||||
if (week.week >= this.xAxisMin - oneWeek && week.week <= this.xAxisMax + oneWeek) {
|
||||
user.total_commits += week.commits;
|
||||
user.total_additions += week.additions;
|
||||
user.total_deletions += week.deletions;
|
||||
if (week[this.type] > user.max_contribution_type) {
|
||||
user.max_contribution_type = week[this.type];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// this line is required. See https://github.com/sahinakkaya/gitea/pull/3#discussion_r1396495722
|
||||
// for details.
|
||||
user.max_contribution_type += 1;
|
||||
|
||||
filteredData[key] = {...user, weeks: filteredWeeks};
|
||||
}
|
||||
|
||||
return filteredData;
|
||||
},
|
||||
|
||||
maxMainGraph() {
|
||||
// This method calculates maximum value for Y value of the main graph. If the number
|
||||
// of maximum contributions for selected contribution type is 15.955 it is probably
|
||||
// better to round it up to 20.000.This method is responsible for doing that.
|
||||
// Normally, chartjs handles this automatically, but it will resize the graph when you
|
||||
// zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
|
||||
const maxValue = Math.max(
|
||||
...this.totalStats.weeks.map((o) => o[this.type])
|
||||
);
|
||||
const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
|
||||
if (coefficient % 1 === 0) return maxValue;
|
||||
return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
|
||||
},
|
||||
|
||||
maxContributorGraph() {
|
||||
// Similar to maxMainGraph method this method calculates maximum value for Y value
|
||||
// for contributors' graph. If I let chartjs do this for me, it will choose different
|
||||
// maxY value for each contributors' graph which again makes it harder to compare.
|
||||
const maxValue = Math.max(
|
||||
...this.sortedContributors.map((c) => c.max_contribution_type)
|
||||
);
|
||||
const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
|
||||
if (coefficient % 1 === 0) return maxValue;
|
||||
return (1 - (coefficient % 1)) * 10 ** exp + maxValue;
|
||||
},
|
||||
|
||||
toGraphData(data) {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
data: data.map((i) => ({x: i.week, y: i[this.type]})),
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 0,
|
||||
fill: 'start',
|
||||
backgroundColor: colors[this.type],
|
||||
borderWidth: 0,
|
||||
tension: 0.3,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
updateOtherCharts(event, reset) {
|
||||
const minVal = event.chart.options.scales.x.min;
|
||||
const maxVal = event.chart.options.scales.x.max;
|
||||
if (reset) {
|
||||
this.xAxisMin = this.xAxisStart;
|
||||
this.xAxisMax = this.xAxisEnd;
|
||||
this.sortContributors();
|
||||
} else if (minVal) {
|
||||
this.xAxisMin = minVal;
|
||||
this.xAxisMax = maxVal;
|
||||
this.sortContributors();
|
||||
}
|
||||
},
|
||||
|
||||
getOptions(type) {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove', 'dblclick'],
|
||||
plugins: {
|
||||
title: {
|
||||
display: type === 'main',
|
||||
text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
|
||||
color: colors.title,
|
||||
position: 'top',
|
||||
align: 'center',
|
||||
},
|
||||
customEventListener: {
|
||||
chartType: type,
|
||||
instance: this,
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
modifierKey: 'shift',
|
||||
mode: 'x',
|
||||
threshold: 20,
|
||||
onPanComplete: this.updateOtherCharts,
|
||||
},
|
||||
limits: {
|
||||
x: {
|
||||
// Check https://www.chartjs.org/chartjs-plugin-zoom/latest/guide/options.html#scale-limits
|
||||
// to know what each option means
|
||||
min: 'original',
|
||||
max: 'original',
|
||||
|
||||
// number of milliseconds in 2 weeks. Minimum x range will be 2 weeks when you zoom on the graph
|
||||
minRange: 2 * 7 * 24 * 60 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
zoom: {
|
||||
drag: {
|
||||
enabled: type === 'main',
|
||||
},
|
||||
pinch: {
|
||||
enabled: type === 'main',
|
||||
},
|
||||
mode: 'x',
|
||||
onZoomComplete: this.updateOtherCharts,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
min: this.xAxisMin,
|
||||
max: this.xAxisMax,
|
||||
type: 'time',
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
time: {
|
||||
minUnit: 'month',
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
maxTicksLimit: type === 'main' ? 12 : 6,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
max: type === 'main' ? this.maxMainGraph() : this.maxContributorGraph(),
|
||||
ticks: {
|
||||
maxTicksLimit: type === 'main' ? 6 : 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="ui header gt-df gt-ac gt-sb">
|
||||
<div>
|
||||
<relative-time
|
||||
v-if="xAxisMin > 0"
|
||||
format="datetime"
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="numeric"
|
||||
weekday=""
|
||||
:datetime="new Date(xAxisMin)"
|
||||
>
|
||||
{{ new Date(xAxisMin) }}
|
||||
</relative-time>
|
||||
{{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "-" }}
|
||||
<relative-time
|
||||
v-if="xAxisMax > 0"
|
||||
format="datetime"
|
||||
year="numeric"
|
||||
month="short"
|
||||
day="numeric"
|
||||
weekday=""
|
||||
:datetime="new Date(xAxisMax)"
|
||||
>
|
||||
{{ new Date(xAxisMax) }}
|
||||
</relative-time>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Contribution type -->
|
||||
<div class="ui dropdown jump" id="repo-contributors">
|
||||
<div class="ui basic compact button">
|
||||
<span class="text">
|
||||
{{ locale.filterLabel }} <strong>{{ locale.contributionType[type] }}</strong>
|
||||
<svg-icon name="octicon-triangle-down" :size="14"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div :class="['item', {'active': type === 'commits'}]">
|
||||
{{ locale.contributionType.commits }}
|
||||
</div>
|
||||
<div :class="['item', {'active': type === 'additions'}]">
|
||||
{{ locale.contributionType.additions }}
|
||||
</div>
|
||||
<div :class="['item', {'active': type === 'deletions'}]">
|
||||
{{ locale.contributionType.deletions }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
<div class="gt-df ui segment main-graph">
|
||||
<div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
|
||||
<div v-if="isLoading">
|
||||
<SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
|
||||
{{ locale.loadingInfo }}
|
||||
</div>
|
||||
<div v-else class="text red">
|
||||
<SvgIcon name="octicon-x-circle-fill"/>
|
||||
{{ errorText }}
|
||||
</div>
|
||||
</div>
|
||||
<ChartLine
|
||||
v-memo="[totalStats.weeks, type]" v-if="Object.keys(totalStats).length !== 0"
|
||||
:data="toGraphData(totalStats.weeks)" :options="getOptions('main')"
|
||||
/>
|
||||
</div>
|
||||
<div class="contributor-grid">
|
||||
<div
|
||||
v-for="(contributor, index) in sortedContributors" :key="index" class="stats-table"
|
||||
v-memo="[sortedContributors, type]"
|
||||
>
|
||||
<div class="ui top attached header gt-df gt-f1">
|
||||
<b class="ui right">#{{ index + 1 }}</b>
|
||||
<a :href="contributor.home_link">
|
||||
<img class="ui avatar gt-vm" height="40" width="40" :src="contributor.avatar_link">
|
||||
</a>
|
||||
<div class="gt-ml-3">
|
||||
<a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a>
|
||||
<h4 v-else class="contributor-name">
|
||||
{{ contributor.name }}
|
||||
</h4>
|
||||
<p class="gt-font-12 gt-df gt-gap-2">
|
||||
<strong v-if="contributor.total_commits">{{ contributor.total_commits.toLocaleString() }} {{ locale.contributionType.commits }}</strong>
|
||||
<strong v-if="contributor.total_additions" class="text green">{{ contributor.total_additions.toLocaleString() }}++ </strong>
|
||||
<strong v-if="contributor.total_deletions" class="text red">
|
||||
{{ contributor.total_deletions.toLocaleString() }}--</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui attached segment">
|
||||
<div>
|
||||
<ChartLine
|
||||
:data="toGraphData(contributor.weeks)"
|
||||
:options="getOptions('contributor')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.main-graph {
|
||||
height: 260px;
|
||||
}
|
||||
.contributor-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.contributor-name {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,14 +1,13 @@
|
|||
import $ from 'jquery';
|
||||
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
||||
import {toggleElem} from '../utils/dom.js';
|
||||
|
||||
export function initCommonOrganization() {
|
||||
if ($('.organization').length === 0) {
|
||||
if (!document.querySelectorAll('.organization').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.organization.settings.options #org_name').on('input', function () {
|
||||
const nameChanged = $(this).val().toLowerCase() !== $(this).attr('data-org-name').toLowerCase();
|
||||
document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () {
|
||||
const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase();
|
||||
toggleElem('#org-name-change-prompt', nameChanged);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
export function handleGlobalEnterQuickSubmit(target) {
|
||||
const form = target.closest('form');
|
||||
if (form) {
|
||||
|
@ -8,14 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (form.classList.contains('form-fetch-action')) {
|
||||
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
|
||||
return;
|
||||
}
|
||||
|
||||
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
|
||||
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
|
||||
$(form).trigger('submit');
|
||||
form.dispatchEvent(new SubmitEvent('submit', {bubbles: true, cancelable: true}));
|
||||
} else {
|
||||
// if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
|
||||
// the 'ce-' prefix means this is a CustomEvent
|
||||
|
|
|
@ -1,43 +1,41 @@
|
|||
import $ from 'jquery';
|
||||
import {POST} from '../../modules/fetch.js';
|
||||
import {hideElem, showElem, toggleElem} from '../../utils/dom.js';
|
||||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
export function initCompWebHookEditor() {
|
||||
if ($('.new.webhook').length === 0) {
|
||||
if (!document.querySelectorAll('.new.webhook').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
$('.events.checkbox input').on('change', function () {
|
||||
if ($(this).is(':checked')) {
|
||||
showElem($('.events.fields'));
|
||||
}
|
||||
});
|
||||
$('.non-events.checkbox input').on('change', function () {
|
||||
if ($(this).is(':checked')) {
|
||||
hideElem($('.events.fields'));
|
||||
}
|
||||
});
|
||||
for (const input of document.querySelectorAll('.events.checkbox input')) {
|
||||
input.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
showElem('.events.fields');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const input of document.querySelectorAll('.non-events.checkbox input')) {
|
||||
input.addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
hideElem('.events.fields');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const updateContentType = function () {
|
||||
const visible = $('#http_method').val() === 'POST';
|
||||
toggleElem($('#content_type').parent().parent(), visible);
|
||||
const visible = document.getElementById('http_method').value === 'POST';
|
||||
toggleElem(document.getElementById('content_type').parentNode.parentNode, visible);
|
||||
};
|
||||
updateContentType();
|
||||
$('#http_method').on('change', () => {
|
||||
updateContentType();
|
||||
});
|
||||
|
||||
document.getElementById('http_method').addEventListener('change', updateContentType);
|
||||
|
||||
// Test delivery
|
||||
$('#test-delivery').on('click', function () {
|
||||
const $this = $(this);
|
||||
$this.addClass('loading disabled');
|
||||
$.post($this.data('link'), {
|
||||
_csrf: csrfToken
|
||||
}).done(
|
||||
setTimeout(() => {
|
||||
window.location.href = $this.data('redirect');
|
||||
}, 5000)
|
||||
);
|
||||
document.getElementById('test-delivery')?.addEventListener('click', async function () {
|
||||
this.classList.add('loading', 'disabled');
|
||||
await POST(this.getAttribute('data-link'));
|
||||
setTimeout(() => {
|
||||
window.location.href = this.getAttribute('data-redirect');
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import $ from 'jquery';
|
||||
import {createApp} from 'vue';
|
||||
import ContextPopup from '../components/ContextPopup.vue';
|
||||
import {parseIssueHref} from '../utils.js';
|
||||
import {createTippy} from '../modules/tippy.js';
|
||||
|
||||
export function initContextPopups() {
|
||||
const refIssues = $('.ref-issue');
|
||||
const refIssues = document.querySelectorAll('.ref-issue');
|
||||
attachRefIssueContextPopup(refIssues);
|
||||
}
|
||||
|
||||
|
|
28
web_src/js/features/contributors.js
Normal file
28
web_src/js/features/contributors.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {createApp} from 'vue';
|
||||
|
||||
export async function initRepoContributors() {
|
||||
const el = document.getElementById('repo-contributors-chart');
|
||||
if (!el) return;
|
||||
|
||||
const {default: RepoContributors} = await import(/* webpackChunkName: "contributors-graph" */'../components/RepoContributors.vue');
|
||||
try {
|
||||
const View = createApp(RepoContributors, {
|
||||
locale: {
|
||||
filterLabel: el.getAttribute('data-locale-filter-label'),
|
||||
contributionType: {
|
||||
commits: el.getAttribute('data-locale-contribution-type-commits'),
|
||||
additions: el.getAttribute('data-locale-contribution-type-additions'),
|
||||
deletions: el.getAttribute('data-locale-contribution-type-deletions'),
|
||||
},
|
||||
|
||||
loadingTitle: el.getAttribute('data-locale-loading-title'),
|
||||
loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
|
||||
loadingInfo: el.getAttribute('data-locale-loading-info'),
|
||||
}
|
||||
});
|
||||
View.mount(el);
|
||||
} catch (err) {
|
||||
console.error('RepoContributors failed to load', err);
|
||||
el.textContent = el.getAttribute('data-locale-component-failed-to-load');
|
||||
}
|
||||
}
|
|
@ -194,7 +194,7 @@ export function initRepoCodeView() {
|
|||
const blob = await $.get(`${url}?${query}&anchor=${anchor}`);
|
||||
currentTarget.closest('tr').outerHTML = blob;
|
||||
});
|
||||
$(document).on('click', '.copy-line-permalink', async (e) => {
|
||||
await clippie(toAbsoluteUrl(e.currentTarget.getAttribute('data-url')));
|
||||
$(document).on('click', '.copy-line-permalink', async ({currentTarget}) => {
|
||||
await clippie(toAbsoluteUrl(currentTarget.getAttribute('data-url')));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -69,16 +69,12 @@ function initRepoIssueListCheckboxes() {
|
|||
}
|
||||
}
|
||||
|
||||
updateIssuesMeta(
|
||||
url,
|
||||
action,
|
||||
issueIDs,
|
||||
elementId,
|
||||
).then(() => {
|
||||
try {
|
||||
await updateIssuesMeta(url, action, issueIDs, elementId);
|
||||
window.location.reload();
|
||||
}).catch((reason) => {
|
||||
showErrorToast(reason.responseJSON.error);
|
||||
});
|
||||
} catch (err) {
|
||||
showErrorToast(err.responseJSON?.error ?? err.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -344,19 +344,15 @@ export async function updateIssuesMeta(url, action, issueIds, elementId) {
|
|||
export function initRepoIssueComments() {
|
||||
if ($('.repository.view.issue .timeline').length === 0) return;
|
||||
|
||||
$('.re-request-review').on('click', function (e) {
|
||||
$('.re-request-review').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
const url = $(this).data('update-url');
|
||||
const issueId = $(this).data('issue-id');
|
||||
const id = $(this).data('id');
|
||||
const isChecked = $(this).hasClass('checked');
|
||||
|
||||
updateIssuesMeta(
|
||||
url,
|
||||
isChecked ? 'detach' : 'attach',
|
||||
issueId,
|
||||
id,
|
||||
).then(() => window.location.reload());
|
||||
await updateIssuesMeta(url, isChecked ? 'detach' : 'attach', issueId, id);
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
$(document).on('click', (event) => {
|
||||
|
|
|
@ -205,12 +205,15 @@ export function initRepoCommentForm() {
|
|||
$listMenu.find('.no-select.item').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (hasUpdateAction) {
|
||||
updateIssuesMeta(
|
||||
$listMenu.data('update-url'),
|
||||
'clear',
|
||||
$listMenu.data('issue-id'),
|
||||
'',
|
||||
).then(reloadConfirmDraftComment);
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
$listMenu.data('update-url'),
|
||||
'clear',
|
||||
$listMenu.data('issue-id'),
|
||||
'',
|
||||
);
|
||||
reloadConfirmDraftComment();
|
||||
})();
|
||||
}
|
||||
|
||||
$(this).parent().find('.item').each(function () {
|
||||
|
@ -248,12 +251,15 @@ export function initRepoCommentForm() {
|
|||
|
||||
$(this).addClass('selected active');
|
||||
if (hasUpdateAction) {
|
||||
updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
).then(reloadConfirmDraftComment);
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
);
|
||||
reloadConfirmDraftComment();
|
||||
})();
|
||||
}
|
||||
|
||||
let icon = '';
|
||||
|
@ -281,12 +287,15 @@ export function initRepoCommentForm() {
|
|||
});
|
||||
|
||||
if (hasUpdateAction) {
|
||||
updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
).then(reloadConfirmDraftComment);
|
||||
(async () => {
|
||||
await updateIssuesMeta(
|
||||
$menu.data('update-url'),
|
||||
'',
|
||||
$menu.data('issue-id'),
|
||||
$(this).data('id'),
|
||||
);
|
||||
reloadConfirmDraftComment();
|
||||
})();
|
||||
}
|
||||
|
||||
$list.find('.selected').html('');
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import $ from 'jquery';
|
||||
|
||||
export function initSshKeyFormParser() {
|
||||
// Parse SSH Key
|
||||
$('#ssh-key-content').on('change paste keyup', function () {
|
||||
const arrays = $(this).val().split(' ');
|
||||
const $title = $('#ssh-key-title');
|
||||
if ($title.val() === '' && arrays.length === 3 && arrays[2] !== '') {
|
||||
$title.val(arrays[2]);
|
||||
// Parse SSH Key
|
||||
document.getElementById('ssh-key-content')?.addEventListener('input', function () {
|
||||
const arrays = this.value.split(' ');
|
||||
const title = document.getElementById('ssh-key-title');
|
||||
if (!title.value && arrays.length === 3 && arrays[2] !== '') {
|
||||
title.value = arrays[2];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import $ from 'jquery';
|
||||
import {hideElem, showElem} from '../utils/dom.js';
|
||||
|
||||
export function initUserSettings() {
|
||||
if ($('.user.settings.profile').length > 0) {
|
||||
$('#username').on('keyup', function () {
|
||||
const $prompt = $('#name-change-prompt');
|
||||
const $prompt_redirect = $('#name-change-redirect-prompt');
|
||||
if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) {
|
||||
showElem($prompt);
|
||||
showElem($prompt_redirect);
|
||||
} else {
|
||||
hideElem($prompt);
|
||||
hideElem($prompt_redirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (document.querySelectorAll('.user.settings.profile').length === 0) return;
|
||||
|
||||
const usernameInput = document.getElementById('username');
|
||||
if (!usernameInput) return;
|
||||
usernameInput.addEventListener('input', function () {
|
||||
const prompt = document.getElementById('name-change-prompt');
|
||||
const promptRedirect = document.getElementById('name-change-redirect-prompt');
|
||||
if (this.value.toLowerCase() !== this.getAttribute('data-name').toLowerCase()) {
|
||||
showElem(prompt);
|
||||
showElem(promptRedirect);
|
||||
} else {
|
||||
hideElem(prompt);
|
||||
hideElem(promptRedirect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import * as htmx from 'htmx.org';
|
||||
import {showErrorToast} from './modules/toast.js';
|
||||
|
||||
// https://github.com/bigskysoftware/idiomorph#htmx
|
||||
import 'idiomorph/dist/idiomorph-ext.js';
|
||||
|
||||
// https://htmx.org/reference/#config
|
||||
htmx.config.requestClass = 'is-loading';
|
||||
htmx.config.scrollIntoViewOnBoost = false;
|
||||
|
|
|
@ -83,6 +83,7 @@ import {initGiteaFomantic} from './modules/fomantic.js';
|
|||
import {onDomReady} from './utils/dom.js';
|
||||
import {initRepoIssueList} from './features/repo-issue-list.js';
|
||||
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
|
||||
import {initRepoContributors} from './features/contributors.js';
|
||||
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
|
||||
import {initDirAuto} from './modules/dirauto.js';
|
||||
|
||||
|
@ -172,6 +173,7 @@ onDomReady(() => {
|
|||
initRepoWikiForm();
|
||||
initRepository();
|
||||
initRepositoryActionView();
|
||||
initRepoContributors();
|
||||
|
||||
initCommitStatuses();
|
||||
initCaptcha();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import $ from 'jquery';
|
||||
import {POST} from '../modules/fetch.js';
|
||||
|
||||
const preventListener = (e) => e.preventDefault();
|
||||
|
||||
|
@ -55,12 +55,11 @@ export function initMarkupTasklist() {
|
|||
const updateUrl = editContentZone.getAttribute('data-update-url');
|
||||
const context = editContentZone.getAttribute('data-context');
|
||||
|
||||
await $.post(updateUrl, {
|
||||
ignore_attachments: true,
|
||||
_csrf: window.config.csrfToken,
|
||||
content: newContent,
|
||||
context
|
||||
});
|
||||
const requestBody = new FormData();
|
||||
requestBody.append('ignore_attachments', 'true');
|
||||
requestBody.append('content', newContent);
|
||||
requestBody.append('context', context);
|
||||
await POST(updateUrl, {data: requestBody});
|
||||
|
||||
rawContent.textContent = newContent;
|
||||
} catch (err) {
|
||||
|
|
|
@ -8,19 +8,17 @@ const safeMethods = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
|||
// fetch wrapper, use below method name functions and the `data` option to pass in data
|
||||
// which will automatically set an appropriate headers. For json content, only object
|
||||
// and array types are currently supported.
|
||||
export function request(url, {method = 'GET', headers = {}, data, body, ...other} = {}) {
|
||||
let contentType;
|
||||
if (!body) {
|
||||
if (data instanceof FormData || data instanceof URLSearchParams) {
|
||||
body = data;
|
||||
} else if (isObject(data) || Array.isArray(data)) {
|
||||
contentType = 'application/json';
|
||||
body = JSON.stringify(data);
|
||||
}
|
||||
export function request(url, {method = 'GET', data, headers = {}, ...other} = {}) {
|
||||
let body, contentType;
|
||||
if (data instanceof FormData || data instanceof URLSearchParams) {
|
||||
body = data;
|
||||
} else if (isObject(data) || Array.isArray(data)) {
|
||||
contentType = 'application/json';
|
||||
body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const headersMerged = new Headers({
|
||||
...(!safeMethods.has(method.toUpperCase()) && {'x-csrf-token': csrfToken}),
|
||||
...(!safeMethods.has(method) && {'x-csrf-token': csrfToken}),
|
||||
...(contentType && {'content-type': contentType}),
|
||||
});
|
||||
|
||||
|
@ -31,8 +29,8 @@ export function request(url, {method = 'GET', headers = {}, data, body, ...other
|
|||
return fetch(url, {
|
||||
method,
|
||||
headers: headersMerged,
|
||||
...(body && {body}),
|
||||
...other,
|
||||
...(body && {body}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
46
web_src/js/utils/time.js
Normal file
46
web_src/js/utils/time.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
// Returns an array of millisecond-timestamps of start-of-week days (Sundays)
|
||||
export function startDaysBetween(startDate, endDate) {
|
||||
// Ensure the start date is a Sunday
|
||||
while (startDate.getDay() !== 0) {
|
||||
startDate.setDate(startDate.getDate() + 1);
|
||||
}
|
||||
|
||||
const start = dayjs(startDate);
|
||||
const end = dayjs(endDate);
|
||||
const startDays = [];
|
||||
|
||||
let current = start;
|
||||
while (current.isBefore(end)) {
|
||||
startDays.push(current.valueOf());
|
||||
// we are adding 7 * 24 hours instead of 1 week because we don't want
|
||||
// date library to use local time zone to calculate 1 week from now.
|
||||
// local time zone is problematic because of daylight saving time (dst)
|
||||
// used on some countries
|
||||
current = current.add(7 * 24, 'hour');
|
||||
}
|
||||
|
||||
return startDays;
|
||||
}
|
||||
|
||||
export function firstStartDateAfterDate(inputDate) {
|
||||
if (!(inputDate instanceof Date)) {
|
||||
throw new Error('Invalid date');
|
||||
}
|
||||
const dayOfWeek = inputDate.getDay();
|
||||
const daysUntilSunday = 7 - dayOfWeek;
|
||||
const resultDate = new Date(inputDate.getTime());
|
||||
resultDate.setDate(resultDate.getDate() + daysUntilSunday);
|
||||
return resultDate.valueOf();
|
||||
}
|
||||
|
||||
export function fillEmptyStartDaysWithZeroes(startDays, data) {
|
||||
const result = {};
|
||||
|
||||
for (const startDay of startDays) {
|
||||
result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0};
|
||||
}
|
||||
|
||||
return Object.values(result);
|
||||
}
|
15
web_src/js/utils/time.test.js
Normal file
15
web_src/js/utils/time.test.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {startDaysBetween} from './time.js';
|
||||
|
||||
test('startDaysBetween', () => {
|
||||
expect(startDaysBetween(new Date('2024-02-15'), new Date('2024-04-18'))).toEqual([
|
||||
1708214400000,
|
||||
1708819200000,
|
||||
1709424000000,
|
||||
1710028800000,
|
||||
1710633600000,
|
||||
1711238400000,
|
||||
1711843200000,
|
||||
1712448000000,
|
||||
1713052800000,
|
||||
]);
|
||||
});
|
|
@ -173,9 +173,13 @@ export default {
|
|||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({ // for htmx extensions
|
||||
htmx: 'htmx.org',
|
||||
}),
|
||||
new DefinePlugin({
|
||||
__VUE_OPTIONS_API__: true, // at the moment, many Vue components still use the Vue Options API
|
||||
__VUE_PROD_DEVTOOLS__: false, // do not enable devtools support in production
|
||||
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, // https://github.com/vuejs/vue-cli/pull/7443
|
||||
}),
|
||||
new VueLoaderPlugin(),
|
||||
new MiniCssExtractPlugin({
|
||||
|
@ -210,6 +214,7 @@ export default {
|
|||
override: {
|
||||
'khroma@*': {licenseName: 'MIT'}, // https://github.com/fabiospampinato/khroma/pull/33
|
||||
'htmx.org@1.9.10': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
|
||||
'idiomorph@0.3.0': {licenseName: 'BSD-2-Clause'}, // "BSD 2-Clause" -> "BSD-2-Clause"
|
||||
},
|
||||
emitError: true,
|
||||
allow: '(Apache-2.0 OR BSD-2-Clause OR BSD-3-Clause OR MIT OR ISC OR CPAL-1.0 OR Unlicense OR EPL-1.0 OR EPL-2.0)',
|
||||
|
|
Loading…
Reference in a new issue