diff --git a/.drone.yml b/.drone.yml index 6ab40a39d..9a27e1c21 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ workspace: clone: git: - image: plugins/git:1 + image: plugins/git:next depth: 50 tags: true @@ -255,6 +255,18 @@ pipeline: when: event: [ push, tag ] + gpg-sign: + image: plugins/gpgsign:1 + pull: true + secrets: [ gpgsign_key, gpgsign_passphrase ] + detach_sign: true + files: + - dist/release/* + excludes: + - dist/release/*.sha256 + when: + event: [ push, tag ] + release: image: plugins/s3:1 pull: true diff --git a/contrib/init/sunos/gitea.xml b/contrib/init/sunos/gitea.xml new file mode 100644 index 000000000..4b8cc3a38 --- /dev/null +++ b/contrib/init/sunos/gitea.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index ef88e5c32..f823f68e4 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -601,9 +601,9 @@ ko-KR = ko [U2F] ; Two Factor authentication with security keys ; https://developers.yubico.com/U2F/App_ID.html -APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s +APP_ID = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ; Comma seperated list of truisted facets -TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s +TRUSTED_FACETS = %(PROTOCOL)s://%(DOMAIN)s:%(HTTP_PORT)s/ ; Extension mapping to highlight class ; e.g. .toml=ini diff --git a/docker/etc/s6/gitea/setup b/docker/etc/s6/gitea/setup index 500cca584..03758ed81 100755 --- a/docker/etc/s6/gitea/setup +++ b/docker/etc/s6/gitea/setup @@ -1,7 +1,5 @@ #!/bin/bash -/usr/sbin/update-ca-certificates - if [ ! -d /data/git/.ssh ]; then mkdir -p /data/git/.ssh chmod 700 /data/git/.ssh diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 981365c71..698de4f46 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -27,659 +27,93 @@ _Symbols used in table:_ * _✘ - unsupported_ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FeatureGiteaGogsGitHub EEGitLab CEGitLab EEBitBucketRhodeCode CE
Open source and free
Issue tracker
Pull/Merge requests
Squash merging
Rebase merging
Pull/Merge request inline comments
Pull/Merge request approval
Merge conflict resolution
Restrict push and merge access to certain users
Markdown support
Issues and pull/merge requests templates
Revert specific commits or a merge request
Labels
Time tracking
Multiple assignees for issues
Related issues
Confidential issues
Comment reactions
Lock Discussion
Batch issue handling
Issue Boards
Create new branches from issues
Commit graph
Web code editor
Branch manager
Create new branches
Repository topics
Repository code search
Global code search
Issue search
Global issue search
Git LFS 2.0
Integrated Git-powered wiki
Static Git-powered pages
Group Milestones
Granular user roles (Code, Issues, Wiki etc)
Cherry-picking changes
GPG Signed Commits
Reject unsigned commits
Verified Committer?
Subgroups: groups within groups
Custom Git Hooks
Repository Activity page
Deploy Tokens
Repository Tokens with write rights
Easy upgrade process
Built-in Container Registry
External git mirroring
AD / LDAP integration
Multiple LDAP / AD server support
LDAP user synchronization
OpenId Connect support?
OAuth 2.0 integration (external authorization)?
Act as OAuth 2.0 provider
Two factor authentication (2FA)
FIDO U2F (2FA)
Webhook support
Mattermost/Slack integration
Discord integration
Built-in CI/CD
External CI/CD status display
Multiple database support
Multiple OS support
Low resource usage (RAM/CPU)
+#### General Features + +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +|---------|-------|------|-----------|-----------|-----------|-----------|--------------| +| Open source and free | ✓ | ✓ | ✘| ✓ | ✘ | ✘ | ✓ | +| Low resource usage (RAM/CPU) | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✘ | +| Multiple database support | ✓ | ✓ | ✘ | ⁄ | ⁄ | ✓ | ✓ | +| Multiple OS support | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | ✓ | +| Easy upgrade process | ✓ | ✓ | ✘ | ✓ | ✓ | ✘ | ✓ | +| Markdown support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Static Git-powered pages | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Integrated Git-powered wiki | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Deploy Tokens | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | +| Built-in Container Registry | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| External git mirroring | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | +| FIDO U2F (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Built-in CI/CD | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Subgroups: groups within groups | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | + +#### Code management + +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +|---------|-------|------|-----------|-----------|-----------|-----------|--------------| +| Repository topics | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Repository code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Global code search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Git LFS 2.0 | ✓ | ✘ | ✓ | ✓ | ✓ | ⁄ | ✓ | +| Group Milestones | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Verified Committer | ✘ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | +| GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Reject unsigned commits | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | +| Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Branch manager | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Create new branches | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Web code editor | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Commit graph | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | + +#### Issue Tracker + +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +|---------|-------|------|-----------|-----------|-----------|-----------|--------------| +| Issue tracker | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Issue templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Labels | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Time tracking | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Multiple assignees for issues | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Related issues | ✘ | ✘ | ⁄ | ✘ | ✓ | ✘ | ✘ | +| Confidential issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Comment reactions | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Lock Discussion | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Batch issue handling | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Issue Boards | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Create new branches from issues | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | +| Issue search | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Global issue search | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | + +#### Pull/Merge requests + +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +|---------|-------|------|-----------|-----------|-----------|-----------|--------------| +| Pull/Merge requests | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Squash merging | ✓ | ✘ | ✓ | ✘ | ✓ | ✓ | ✓ | +| Rebase merging | ✓ | ✓ | ✓ | ✘ | ⁄ | ✘ | ✓ | +| Pull/Merge request inline comments | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Pull/Merge request approval | ✘ | ✘ | ⁄ | ✓ | ✓ | ✓ | ✓ | +| Merge conflict resolution | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Restrict push and merge access to certain users | ✓ | ✘ | ✓ | ⁄ | ✓ | ✓ | ✓ | +| Revert specific commits or a merge request | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Pull/Merge requests templates | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | ✘ | +| Cherry-picking changes | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | + + +#### 3rd-party integrations + +| Feature | Gitea | Gogs | GitHub EE | GitLab CE | GitLab EE | BitBucket | RhodeCode CE | +|---------|-------|------|-----------|-----------|-----------|-----------|--------------| +| Webhook support | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Custom Git Hooks | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| AD / LDAP integration | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Multiple LDAP / AD server support | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | +| LDAP user synchronization | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | +| OpenId Connect support | ✓ | ✘ | ✓ | ✓ | ✓ | ? | ✘ | +| OAuth 2.0 integration (external authorization) | ✓ | ✘ | ⁄ | ✓ | ✓ | ? | ✓ | +| Act as OAuth 2.0 provider | ✘ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Two factor authentication (2FA) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✘ | +| Mattermost/Slack integration | ✓ | ✓ | ⁄ | ✓ | ✓ | ⁄ | ✓ | +| Discord integration | ✓ | ✓ | ✓ | ✘ | ✘ | ✘ | ✘ | +| External CI/CD status display | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/docs/content/doc/installation/with-docker.en-us.md b/docs/content/doc/installation/with-docker.en-us.md index c672393cc..aa4337e08 100644 --- a/docs/content/doc/installation/with-docker.en-us.md +++ b/docs/content/doc/installation/with-docker.en-us.md @@ -245,6 +245,8 @@ You can configure some of Gitea's settings via environment variables: * `SECRET_KEY`: **""**: Global secret key. This should be changed. If this has a value and `INSTALL_LOCK` is empty, `INSTALL_LOCK` will automatically set to `true`. * `DISABLE_REGISTRATION`: **false**: Disable registration, after which only admin can create accounts for users. * `REQUIRE_SIGNIN_VIEW`: **false**: Enable this to force users to log in to view any page. +* `USER_UID`: **1000**: The UID (Unix user ID) of the user that runs Gitea within the container. Match this to the UID of the owner of the `/data` volume if using host volumes (this is not necessary with named volumes). +* `USER_GID`: **1000**: The GID (Unix group ID) of the user that runs Gitea within the container. Match this to the GID of the owner of the `/data` volume if using host volumes (this is not necessary with named volumes). # Customization diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index b766dd584..12429c88a 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -67,9 +67,9 @@ func TestAPISearchRepo(t *testing.T) { expectedResults }{ {name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50", expectedResults: expectedResults{ - nil: {count: 15}, - user: {count: 15}, - user2: {count: 15}}, + nil: {count: 16}, + user: {count: 16}, + user2: {count: 16}}, }, {name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10", expectedResults: expectedResults{ nil: {count: 10}, diff --git a/models/access_test.go b/models/access_test.go index 59575acb7..46d6f723e 100644 --- a/models/access_test.go +++ b/models/access_test.go @@ -22,8 +22,12 @@ func TestAccessLevel(t *testing.T) { user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) user2 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) - repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository) - repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository) + // A public repository owned by User 2 + repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + assert.False(t, repo1.IsPrivate) + // A private repository owned by Org 3 + repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) + assert.True(t, repo2.IsPrivate) level, err := AccessLevel(user1.ID, repo1) assert.NoError(t, err) @@ -47,8 +51,12 @@ func TestHasAccess(t *testing.T) { user1 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) user2 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) - repo1 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 2, IsPrivate: false}).(*Repository) - repo2 := AssertExistsAndLoadBean(t, &Repository{OwnerID: 3, IsPrivate: true}).(*Repository) + // A public repository owned by User 2 + repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + assert.False(t, repo1.IsPrivate) + // A private repository owned by Org 3 + repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 3}).(*Repository) + assert.True(t, repo2.IsPrivate) for _, accessMode := range accessModes { has, err := HasAccess(user1.ID, repo1, accessMode) diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index c7d73fe17..3238b65ea 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -351,7 +351,7 @@ is_mirror: true num_forks: 1 is_fork: false - + - id: 29 fork_id: 27 @@ -365,7 +365,7 @@ num_closed_pulls: 0 is_mirror: false is_fork: true - + - id: 30 fork_id: 28 @@ -389,3 +389,14 @@ num_forks: 0 num_issues: 0 is_mirror: false + +- + id: 32 + owner_id: 3 + lower_name: repo21 + name: repo21 + is_private: false + num_stars: 0 + num_forks: 0 + num_issues: 0 + is_mirror: false diff --git a/models/fixtures/team.yml b/models/fixtures/team.yml index 1d242cb5b..4b4a1d798 100644 --- a/models/fixtures/team.yml +++ b/models/fixtures/team.yml @@ -4,9 +4,8 @@ lower_name: owners name: Owners authorize: 4 # owner - num_repos: 2 + num_repos: 3 num_members: 1 - unit_types: '[1,2,3,4,5,6,7]' - id: 2 @@ -16,7 +15,6 @@ authorize: 2 # write num_repos: 1 num_members: 2 - unit_types: '[1,2,3,4,5,6,7]' - id: 3 @@ -26,7 +24,6 @@ authorize: 4 # owner num_repos: 0 num_members: 1 - unit_types: '[1,2,3,4,5,6,7]' - id: 4 @@ -36,7 +33,6 @@ authorize: 4 # owner num_repos: 0 num_members: 1 - unit_types: '[1,2,3,4,5,6,7]' - id: 5 @@ -46,7 +42,6 @@ authorize: 4 # owner num_repos: 2 num_members: 2 - unit_types: '[1,2,3,4,5,6,7]' - id: 6 @@ -56,4 +51,3 @@ authorize: 4 # owner num_repos: 2 num_members: 1 - unit_types: '[1,2,3,4,5,6,7]' \ No newline at end of file diff --git a/models/fixtures/team_repo.yml b/models/fixtures/team_repo.yml index 9e6d74553..b324e0941 100644 --- a/models/fixtures/team_repo.yml +++ b/models/fixtures/team_repo.yml @@ -33,9 +33,15 @@ org_id: 19 team_id: 6 repo_id: 27 - + - id: 7 org_id: 19 team_id: 6 - repo_id: 28 \ No newline at end of file + repo_id: 28 + +- + id: 8 + org_id: 3 + team_id: 1 + repo_id: 32 diff --git a/models/fixtures/team_unit.yml b/models/fixtures/team_unit.yml new file mode 100644 index 000000000..ad5466a5c --- /dev/null +++ b/models/fixtures/team_unit.yml @@ -0,0 +1,209 @@ +- + id: 1 + team_id: 1 + type: 1 + +- + id: 2 + team_id: 1 + type: 2 + +- + id: 3 + team_id: 1 + type: 3 + +- + id: 4 + team_id: 1 + type: 4 + +- + id: 5 + team_id: 1 + type: 5 + +- + id: 6 + team_id: 1 + type: 6 + +- + id: 7 + team_id: 1 + type: 7 + +- + id: 8 + team_id: 2 + type: 1 + +- + id: 9 + team_id: 2 + type: 2 + +- + id: 10 + team_id: 2 + type: 3 + +- + id: 11 + team_id: 2 + type: 4 + +- + id: 12 + team_id: 2 + type: 5 + +- + id: 13 + team_id: 2 + type: 6 + +- + id: 14 + team_id: 2 + type: 7 + +- + id: 15 + team_id: 3 + type: 1 + +- + id: 16 + team_id: 3 + type: 2 + +- + id: 17 + team_id: 3 + type: 3 + +- + id: 18 + team_id: 3 + type: 4 + +- + id: 19 + team_id: 3 + type: 5 + +- + id: 20 + team_id: 3 + type: 6 + +- + id: 21 + team_id: 3 + type: 7 + +- + id: 22 + team_id: 4 + type: 1 + +- + id: 23 + team_id: 4 + type: 2 + +- + id: 24 + team_id: 4 + type: 3 + +- + id: 25 + team_id: 4 + type: 4 + +- + id: 26 + team_id: 4 + type: 5 + +- + id: 27 + team_id: 4 + type: 6 + +- + id: 28 + team_id: 4 + type: 7 + +- + id: 29 + team_id: 5 + type: 1 + +- + id: 30 + team_id: 5 + type: 2 + +- + id: 31 + team_id: 5 + type: 3 + +- + id: 32 + team_id: 5 + type: 4 + +- + id: 33 + team_id: 5 + type: 5 + +- + id: 34 + team_id: 5 + type: 6 + +- + id: 35 + team_id: 5 + type: 7 + +- + id: 36 + team_id: 6 + type: 1 + +- + id: 37 + team_id: 6 + type: 2 + +- + id: 38 + team_id: 6 + type: 3 + +- + id: 39 + team_id: 6 + type: 4 + +- + id: 40 + team_id: 6 + type: 5 + +- + id: 41 + team_id: 6 + type: 6 + +- + id: 42 + team_id: 6 + type: 7 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 7ad48f7fb..a2e3b88d7 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -45,7 +45,7 @@ is_admin: false avatar: avatar3 avatar_email: user3@example.com - num_repos: 2 + num_repos: 3 num_members: 2 num_teams: 2 diff --git a/models/issue_watch.go b/models/issue_watch.go index 69e218af0..3e7d24821 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -71,3 +71,15 @@ func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error Find(&watches) return } + +func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error { + iw := &IssueWatch{ + IsWatching: false, + } + _, err := e. + Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID). + Cols("is_watching", "updated_unix"). + Where("`issue_watch`.user_id = ?", userID). + Update(iw) + return err +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 1300065ab..cc262d810 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -186,6 +186,12 @@ var migrations = []Migration{ NewMigration("add u2f", addU2FReg), // v66 -> v67 NewMigration("add login source id column for public_key table", addLoginSourceIDToPublicKeyTable), + // v67 -> v68 + NewMigration("remove stale watches", removeStaleWatches), + // v68 -> V69 + NewMigration("Reformat and remove incorrect topics", reformatAndRemoveIncorrectTopics), + // v69 -> v70 + NewMigration("move team units to team_unit table", moveTeamUnitsToTeamUnitTable), } // Migrate database to current version diff --git a/models/migrations/v38.go b/models/migrations/v38.go index 6f66484b0..eb90f9fbf 100644 --- a/models/migrations/v38.go +++ b/models/migrations/v38.go @@ -25,10 +25,15 @@ func removeCommitsUnitType(x *xorm.Engine) (err error) { Created time.Time `xorm:"-"` } + type Team struct { + ID int64 + UnitTypes []int `xorm:"json"` + } + // Update team unit types const batchSize = 100 for start := 0; ; start += batchSize { - teams := make([]*models.Team, 0, batchSize) + teams := make([]*Team, 0, batchSize) if err := x.Limit(batchSize, start).Find(&teams); err != nil { return err } @@ -36,7 +41,7 @@ func removeCommitsUnitType(x *xorm.Engine) (err error) { break } for _, team := range teams { - ut := make([]models.UnitType, 0, len(team.UnitTypes)) + ut := make([]int, 0, len(team.UnitTypes)) for _, u := range team.UnitTypes { if u < V16UnitTypeCommits { ut = append(ut, u) diff --git a/models/migrations/v67.go b/models/migrations/v67.go new file mode 100644 index 000000000..278221919 --- /dev/null +++ b/models/migrations/v67.go @@ -0,0 +1,158 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/go-xorm/xorm" +) + +func removeStaleWatches(x *xorm.Engine) error { + type Watch struct { + ID int64 + UserID int64 + RepoID int64 + } + + type IssueWatch struct { + ID int64 + UserID int64 + RepoID int64 + IsWatching bool + } + + type Repository struct { + ID int64 + IsPrivate bool + OwnerID int64 + } + + type Access struct { + UserID int64 + RepoID int64 + Mode int + } + + const ( + // AccessModeNone no access + AccessModeNone int = iota // 0 + // AccessModeRead read access + AccessModeRead // 1 + ) + + accessLevel := func(userID int64, repo *Repository) (int, error) { + mode := AccessModeNone + if !repo.IsPrivate { + mode = AccessModeRead + } + + if userID == 0 { + return mode, nil + } + + if userID == repo.OwnerID { + return 4, nil + } + + a := &Access{UserID: userID, RepoID: repo.ID} + if has, err := x.Get(a); !has || err != nil { + return mode, err + } + return a.Mode, nil + } + + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return err + } + + repoCache := make(map[int64]*Repository) + err := x.BufferSize(setting.IterateBufferSize).Iterate(new(Watch), + func(idx int, bean interface{}) error { + watch := bean.(*Watch) + + repo := repoCache[watch.RepoID] + if repo == nil { + repo = &Repository{ + ID: watch.RepoID, + } + if _, err := x.Get(repo); err != nil { + return err + } + repoCache[watch.RepoID] = repo + } + + // Remove watches from now unaccessible repositories + mode, err := accessLevel(watch.UserID, repo) + if err != nil { + return err + } + has := AccessModeRead <= mode + if has { + return nil + } + + if _, err = sess.Delete(&Watch{0, watch.UserID, repo.ID}); err != nil { + return err + } + _, err = sess.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repo.ID) + + return err + }) + if err != nil { + return err + } + + repoCache = make(map[int64]*Repository) + err = x.BufferSize(setting.IterateBufferSize). + Distinct("issue_watch.user_id", "issue.repo_id"). + Join("INNER", "issue", "issue_watch.issue_id = issue.id"). + Where("issue_watch.is_watching = ?", true). + Iterate(new(IssueWatch), + func(idx int, bean interface{}) error { + watch := bean.(*IssueWatch) + + repo := repoCache[watch.RepoID] + if repo == nil { + repo = &Repository{ + ID: watch.RepoID, + } + if _, err := x.Get(repo); err != nil { + return err + } + repoCache[watch.RepoID] = repo + } + + // Remove issue watches from now unaccssible repositories + mode, err := accessLevel(watch.UserID, repo) + if err != nil { + return err + } + has := AccessModeRead <= mode + if has { + return nil + } + + iw := &IssueWatch{ + IsWatching: false, + } + + _, err = sess. + Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", watch.RepoID). + Cols("is_watching", "updated_unix"). + Where("`issue_watch`.user_id = ?", watch.UserID). + Update(iw) + + return err + + }) + if err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/migrations/v68.go b/models/migrations/v68.go new file mode 100644 index 000000000..d6a0d04c5 --- /dev/null +++ b/models/migrations/v68.go @@ -0,0 +1,160 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + + "github.com/go-xorm/xorm" +) + +func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) { + log.Info("This migration could take up to minutes, please be patient.") + type Topic struct { + ID int64 + Name string `xorm:"unique"` + } + + sess := x.NewSession() + defer sess.Close() + + const batchSize = 100 + touchedRepo := make(map[int64]struct{}) + topics := make([]*Topic, 0, batchSize) + delTopicIDs := make([]int64, 0, batchSize) + ids := make([]int64, 0, 30) + + if err := sess.Begin(); err != nil { + return err + } + log.Info("Validating existed topics...") + for start := 0; ; start += batchSize { + topics = topics[:0] + if err := sess.Asc("id").Limit(batchSize, start).Find(&topics); err != nil { + return err + } + if len(topics) == 0 { + break + } + for _, topic := range topics { + if models.ValidateTopic(topic.Name) { + continue + } + topic.Name = strings.Replace(strings.TrimSpace(strings.ToLower(topic.Name)), " ", "-", -1) + + if err := sess.Table("repo_topic").Cols("repo_id"). + Where("topic_id = ?", topic.ID).Find(&ids); err != nil { + return err + } + for _, id := range ids { + touchedRepo[id] = struct{}{} + } + + if models.ValidateTopic(topic.Name) { + log.Info("Updating topic: id = %v, name = %v", topic.ID, topic.Name) + if _, err := sess.Table("topic").ID(topic.ID). + Update(&Topic{Name: topic.Name}); err != nil { + return err + } + } else { + delTopicIDs = append(delTopicIDs, topic.ID) + } + } + } + + log.Info("Deleting incorrect topics...") + for start := 0; ; start += batchSize { + if (start + batchSize) < len(delTopicIDs) { + ids = delTopicIDs[start:(start + batchSize)] + } else { + ids = delTopicIDs[start:] + } + + log.Info("Deleting 'repo_topic' rows for topics with ids = %v", ids) + if _, err := sess.In("topic_id", ids).Delete(&models.RepoTopic{}); err != nil { + return err + } + + log.Info("Deleting topics with id = %v", ids) + if _, err := sess.In("id", ids).Delete(&Topic{}); err != nil { + return err + } + + if len(ids) < batchSize { + break + } + } + + repoTopics := make([]*models.RepoTopic, 0, batchSize) + delRepoTopics := make([]*models.RepoTopic, 0, batchSize) + tmpRepoTopics := make([]*models.RepoTopic, 0, 30) + + log.Info("Checking the number of topics in the repositories...") + for start := 0; ; start += batchSize { + repoTopics = repoTopics[:0] + if err := sess.Cols("repo_id").Asc("repo_id").Limit(batchSize, start). + GroupBy("repo_id").Having("COUNT(*) > 25").Find(&repoTopics); err != nil { + return err + } + if len(repoTopics) == 0 { + break + } + + log.Info("Number of repositories with more than 25 topics: %v", len(repoTopics)) + for _, repoTopic := range repoTopics { + touchedRepo[repoTopic.RepoID] = struct{}{} + + tmpRepoTopics = tmpRepoTopics[:0] + if err := sess.Where("repo_id = ?", repoTopic.RepoID).Find(&tmpRepoTopics); err != nil { + return err + } + + log.Info("Repository with id = %v has %v topics", repoTopic.RepoID, len(tmpRepoTopics)) + + for i := len(tmpRepoTopics) - 1; i > 24; i-- { + delRepoTopics = append(delRepoTopics, tmpRepoTopics[i]) + } + } + } + + log.Info("Deleting superfluous topics for repositories (more than 25 topics)...") + for _, repoTopic := range delRepoTopics { + log.Info("Deleting 'repo_topic' rows for 'repository' with id = %v. Topic id = %v", + repoTopic.RepoID, repoTopic.TopicID) + + if _, err := sess.Where("repo_id = ? AND topic_id = ?", repoTopic.RepoID, + repoTopic.TopicID).Delete(&models.RepoTopic{}); err != nil { + return err + } + if _, err := sess.Exec( + "UPDATE topic SET repo_count = (SELECT repo_count FROM topic WHERE id = ?) - 1 WHERE id = ?", + repoTopic.TopicID, repoTopic.TopicID); err != nil { + return err + } + } + + topicNames := make([]string, 0, 30) + log.Info("Updating repositories 'topics' fields...") + for repoID := range touchedRepo { + if err := sess.Table("topic").Cols("name"). + Join("INNER", "repo_topic", "topic.id = repo_topic.topic_id"). + Where("repo_topic.repo_id = ?", repoID).Find(&topicNames); err != nil { + return err + } + log.Info("Updating 'topics' field for repository with id = %v", repoID) + if _, err := sess.ID(repoID).Cols("topics"). + Update(&models.Repository{Topics: topicNames}); err != nil { + return err + } + } + if err := sess.Commit(); err != nil { + return err + } + + return nil +} diff --git a/models/migrations/v69.go b/models/migrations/v69.go new file mode 100644 index 000000000..8d964688a --- /dev/null +++ b/models/migrations/v69.go @@ -0,0 +1,80 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "github.com/go-xorm/xorm" +) + +func moveTeamUnitsToTeamUnitTable(x *xorm.Engine) error { + // Team see models/team.go + type Team struct { + ID int64 + OrgID int64 + UnitTypes []int `xorm:"json"` + } + + // TeamUnit see models/org_team.go + type TeamUnit struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + Type int `xorm:"UNIQUE(s)"` + } + + if err := x.Sync2(new(TeamUnit)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + + // Update team unit types + const batchSize = 100 + for start := 0; ; start += batchSize { + teams := make([]*Team, 0, batchSize) + if err := x.Limit(batchSize, start).Find(&teams); err != nil { + return err + } + if len(teams) == 0 { + break + } + + for _, team := range teams { + var unitTypes []int + if len(team.UnitTypes) == 0 { + unitTypes = allUnitTypes + } else { + unitTypes = team.UnitTypes + } + + // insert units for team + var units = make([]TeamUnit, 0, len(unitTypes)) + for _, tp := range unitTypes { + units = append(units, TeamUnit{ + OrgID: team.OrgID, + TeamID: team.ID, + Type: tp, + }) + } + + if _, err := sess.Insert(&units); err != nil { + return fmt.Errorf("Insert team units: %v", err) + } + + } + } + + if err := dropTableColumns(sess, "team", "unit_types"); err != nil { + return err + } + return sess.Commit() +} diff --git a/models/models.go b/models/models.go index ddf784dee..aaf1370fd 100644 --- a/models/models.go +++ b/models/models.go @@ -1,4 +1,5 @@ // Copyright 2014 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -121,6 +122,7 @@ func init() { new(Reaction), new(IssueAssignees), new(U2FRegistration), + new(TeamUnit), ) gonicNames := []string{"SSL", "UID"} @@ -184,6 +186,18 @@ func parsePostgreSQLHostPort(info string) (string, string) { return host, port } +func getPostgreSQLConnectionString(DBHost, DBUser, DBPasswd, DBName, DBParam, DBSSLMode string) (connStr string) { + host, port := parsePostgreSQLHostPort(DBHost) + if host[0] == '/' { // looks like a unix socket + connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s", + url.PathEscape(DBUser), url.PathEscape(DBPasswd), port, DBName, DBParam, DBSSLMode, host) + } else { + connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s", + url.PathEscape(DBUser), url.PathEscape(DBPasswd), host, port, DBName, DBParam, DBSSLMode) + } + return +} + func parseMSSQLHostPort(info string) (string, string) { host, port := "127.0.0.1", "1433" if strings.Contains(info, ":") { @@ -214,14 +228,7 @@ func getEngine() (*xorm.Engine, error) { DbCfg.User, DbCfg.Passwd, DbCfg.Host, DbCfg.Name, Param) } case "postgres": - host, port := parsePostgreSQLHostPort(DbCfg.Host) - if host[0] == '/' { // looks like a unix socket - connStr = fmt.Sprintf("postgres://%s:%s@:%s/%s%ssslmode=%s&host=%s", - url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), port, DbCfg.Name, Param, DbCfg.SSLMode, host) - } else { - connStr = fmt.Sprintf("postgres://%s:%s@%s:%s/%s%ssslmode=%s", - url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Passwd), host, port, DbCfg.Name, Param, DbCfg.SSLMode) - } + connStr = getPostgreSQLConnectionString(DbCfg.Host, DbCfg.User, DbCfg.Passwd, DbCfg.Name, Param, DbCfg.SSLMode) case "mssql": host, port := parseMSSQLHostPort(DbCfg.Host) connStr = fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", host, port, DbCfg.Name, DbCfg.User, DbCfg.Passwd) diff --git a/models/models_test.go b/models/models_test.go index 649b1e02e..7016fdb4b 100644 --- a/models/models_test.go +++ b/models/models_test.go @@ -1,4 +1,5 @@ // Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -53,3 +54,42 @@ func Test_parsePostgreSQLHostPort(t *testing.T) { assert.Equal(t, test.Port, port) } } + +func Test_getPostgreSQLConnectionString(t *testing.T) { + tests := []struct { + Host string + Port string + User string + Passwd string + Name string + Param string + SSLMode string + Output string + }{ + { + Host: "/tmp/pg.sock", + Port: "4321", + User: "testuser", + Passwd: "space space !#$%^^%^```-=?=", + Name: "gitea", + Param: "", + SSLMode: "false", + Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/giteasslmode=false&host=/tmp/pg.sock", + }, + { + Host: "localhost", + Port: "1234", + User: "pgsqlusername", + Passwd: "I love Gitea!", + Name: "gitea", + Param: "", + SSLMode: "true", + Output: "postgres://pgsqlusername:I%20love%20Gitea%21@localhost:5432/giteasslmode=true", + }, + } + + for _, test := range tests { + connStr := getPostgreSQLConnectionString(test.Host, test.User, test.Passwd, test.Name, test.Param, test.SSLMode) + assert.Equal(t, test.Output, connStr) + } +} diff --git a/models/notification.go b/models/notification.go index c8376a857..8c36c0c5c 100644 --- a/models/notification.go +++ b/models/notification.go @@ -119,7 +119,17 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor } } + issue.loadRepo(e) + for _, watch := range watches { + issue.Repo.Units = nil + if issue.IsPull && !issue.Repo.CheckUnitUser(watch.UserID, false, UnitTypePullRequests) { + continue + } + if !issue.IsPull && !issue.Repo.CheckUnitUser(watch.UserID, false, UnitTypeIssues) { + continue + } + if err := notifyUser(watch.UserID); err != nil { return err } diff --git a/models/org.go b/models/org.go index ed0d58306..23f6c58bf 100644 --- a/models/org.go +++ b/models/org.go @@ -154,12 +154,26 @@ func CreateOrganization(org, owner *User) (err error) { Name: ownerTeamName, Authorize: AccessModeOwner, NumMembers: 1, - UnitTypes: allRepUnitTypes, } if _, err = sess.Insert(t); err != nil { return fmt.Errorf("insert owner team: %v", err) } + // insert units for team + var units = make([]TeamUnit, 0, len(allRepUnitTypes)) + for _, tp := range allRepUnitTypes { + units = append(units, TeamUnit{ + OrgID: org.ID, + TeamID: t.ID, + Type: tp, + }) + } + + if _, err = sess.Insert(&units); err != nil { + sess.Rollback() + return err + } + if _, err = sess.Insert(&TeamUser{ UID: owner.ID, OrgID: org.ID, @@ -238,6 +252,7 @@ func deleteOrg(e *xorm.Session, u *User) error { &Team{OrgID: u.ID}, &OrgUser{OrgID: u.ID}, &TeamUser{OrgID: u.ID}, + &TeamUnit{OrgID: u.ID}, ); err != nil { return fmt.Errorf("deleteBeans: %v", err) } diff --git a/models/org_team.go b/models/org_team.go index 9d8a03141..3b37f936f 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -1,3 +1,4 @@ +// Copyright 2018 The Gitea Authors. All rights reserved. // Copyright 2016 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -10,7 +11,6 @@ import ( "strings" "code.gitea.io/gitea/modules/log" - "github.com/go-xorm/xorm" ) @@ -28,15 +28,16 @@ type Team struct { Members []*User `xorm:"-"` NumRepos int NumMembers int - UnitTypes []UnitType `xorm:"json"` + Units []*TeamUnit `xorm:"-"` } -// GetUnitTypes returns unit types the team owned, empty means all the unit types -func (t *Team) GetUnitTypes() []UnitType { - if len(t.UnitTypes) == 0 { - return allRepUnitTypes +func (t *Team) getUnits(e Engine) (err error) { + if t.Units != nil { + return nil } - return t.UnitTypes + + t.Units, err = getUnitsByTeamID(e, t.ID) + return err } // HasWriteAccess returns true if team has at least write level access mode. @@ -178,6 +179,11 @@ func (t *Team) removeRepository(e Engine, repo *Repository, recalculate bool) (e if err = watchRepo(e, teamUser.UID, repo.ID, false); err != nil { return err } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := removeIssueWatchersByRepoID(e, teamUser.UID, repo.ID); err != nil { + return err + } } return nil @@ -209,11 +215,12 @@ func (t *Team) RemoveRepository(repoID int64) error { // UnitEnabled returns if the team has the given unit type enabled func (t *Team) UnitEnabled(tp UnitType) bool { - if len(t.UnitTypes) == 0 { - return true + if err := t.getUnits(x); err != nil { + log.Warn("Error loading repository (ID: %d) units: %s", t.ID, err.Error()) } - for _, u := range t.UnitTypes { - if u == tp { + + for _, unit := range t.Units { + if unit.Type == tp { return true } } @@ -270,6 +277,17 @@ func NewTeam(t *Team) (err error) { return err } + // insert units for team + if len(t.Units) > 0 { + for _, unit := range t.Units { + unit.TeamID = t.ID + } + if _, err = sess.Insert(&t.Units); err != nil { + sess.Rollback() + return err + } + } + // Update organization number of teams. if _, err = sess.Exec("UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil { sess.Rollback() @@ -374,11 +392,34 @@ func DeleteTeam(t *Team) error { return err } + if err := t.getMembers(sess); err != nil { + return err + } + // Delete all accesses. for _, repo := range t.Repos { if err := repo.recalculateTeamAccesses(sess, t.ID); err != nil { return err } + + // Remove watches from all users and now unaccessible repos + for _, user := range t.Members { + has, err := hasAccess(sess, user.ID, repo, AccessModeRead) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(sess, user.ID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err = removeIssueWatchersByRepoID(sess, user.ID, repo.ID); err != nil { + return err + } + } } // Delete team-repo @@ -396,6 +437,13 @@ func DeleteTeam(t *Team) error { return err } + // Delete team-unit. + if _, err := sess. + Where("team_id=?", t.ID). + Delete(new(TeamUnit)); err != nil { + return err + } + // Delete team. if _, err := sess.ID(t.ID).Delete(new(Team)); err != nil { return err @@ -518,6 +566,10 @@ func AddTeamMember(team *Team, userID int64) error { if err := repo.recalculateTeamAccesses(sess, 0); err != nil { return err } + + if err = watchRepo(sess, userID, repo.ID, true); err != nil { + return err + } } return sess.Commit() @@ -558,6 +610,23 @@ func removeTeamMember(e *xorm.Session, team *Team, userID int64) error { if err := repo.recalculateTeamAccesses(e, 0); err != nil { return err } + + // Remove watches from now unaccessible + has, err := hasAccess(e, userID, repo, AccessModeRead) + if err != nil { + return err + } else if has { + continue + } + + if err = watchRepo(e, userID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := removeIssueWatchersByRepoID(e, userID, repo.ID); err != nil { + return err + } } // Check if the user is a member of any team in the organization. @@ -646,3 +715,47 @@ func GetTeamsWithAccessToRepo(orgID, repoID int64, mode AccessMode) ([]*Team, er And("team_repo.repo_id = ?", repoID). Find(&teams) } + +// ___________ ____ ___ .__ __ +// \__ ___/___ _____ _____ | | \____ |__|/ |_ +// | |_/ __ \\__ \ / \| | / \| \ __\ +// | |\ ___/ / __ \| Y Y \ | / | \ || | +// |____| \___ >____ /__|_| /______/|___| /__||__| +// \/ \/ \/ \/ + +// TeamUnit describes all units of a repository +type TeamUnit struct { + ID int64 `xorm:"pk autoincr"` + OrgID int64 `xorm:"INDEX"` + TeamID int64 `xorm:"UNIQUE(s)"` + Type UnitType `xorm:"UNIQUE(s)"` +} + +// Unit returns Unit +func (t *TeamUnit) Unit() Unit { + return Units[t.Type] +} + +func getUnitsByTeamID(e Engine, teamID int64) (units []*TeamUnit, err error) { + return units, e.Where("team_id = ?", teamID).Find(&units) +} + +// UpdateTeamUnits updates a teams's units +func UpdateTeamUnits(team *Team, units []TeamUnit) (err error) { + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + if _, err = sess.Where("team_id = ?", team.ID).Delete(new(TeamUnit)); err != nil { + return err + } + + if _, err = sess.Insert(units); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} diff --git a/models/org_test.go b/models/org_test.go index 42ab4a2a4..c54e7a93b 100644 --- a/models/org_test.go +++ b/models/org_test.go @@ -489,8 +489,8 @@ func TestAccessibleReposEnv_CountRepos(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, expectedCount, count) } - testSuccess(2, 2) - testSuccess(4, 1) + testSuccess(2, 3) + testSuccess(4, 2) } func TestAccessibleReposEnv_RepoIDs(t *testing.T) { @@ -503,8 +503,8 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, expectedRepoIDs, repoIDs) } - testSuccess(2, 1, 100, []int64{3, 5}) - testSuccess(4, 0, 100, []int64{3}) + testSuccess(2, 1, 100, []int64{3, 5, 32}) + testSuccess(4, 0, 100, []int64{3, 32}) } func TestAccessibleReposEnv_Repos(t *testing.T) { @@ -522,8 +522,8 @@ func TestAccessibleReposEnv_Repos(t *testing.T) { } assert.Equal(t, expectedRepos, repos) } - testSuccess(2, []int64{3, 5}) - testSuccess(4, []int64{3}) + testSuccess(2, []int64{3, 5, 32}) + testSuccess(4, []int64{3, 32}) } func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { diff --git a/models/repo.go b/models/repo.go index f4923cf4a..d1cc290c4 100644 --- a/models/repo.go +++ b/models/repo.go @@ -365,22 +365,14 @@ func (repo *Repository) getUnitsByUserID(e Engine, userID int64, isAdmin bool) ( return err } - var allTypes = make(map[UnitType]struct{}, len(allRepUnitTypes)) - for _, team := range teams { - // Administrators can not be limited - if team.Authorize >= AccessModeAdmin { - return nil - } - for _, unitType := range team.UnitTypes { - allTypes[unitType] = struct{}{} - } - } - // unique var newRepoUnits = make([]*RepoUnit, 0, len(repo.Units)) for _, u := range repo.Units { - if _, ok := allTypes[u.Type]; ok { - newRepoUnits = append(newRepoUnits, u) + for _, team := range teams { + if team.UnitEnabled(u.Type) { + newRepoUnits = append(newRepoUnits, u) + break + } } } @@ -1851,6 +1843,9 @@ func DeleteRepository(doer *User, uid, repoID int64) error { if _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}); err != nil { return err } + if _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}); err != nil { + return err + } attachments := make([]*Attachment, 0, 5) if err = sess. diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go index 0448149e6..9d2935d58 100644 --- a/models/repo_collaboration.go +++ b/models/repo_collaboration.go @@ -172,5 +172,14 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) { return err } + if err = watchRepo(sess, uid, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repository + if err := removeIssueWatchersByRepoID(sess, uid, repo.ID); err != nil { + return err + } + return sess.Commit() } diff --git a/models/repo_list_test.go b/models/repo_list_test.go index 3bccb1aeb..164bc19bf 100644 --- a/models/repo_list_test.go +++ b/models/repo_list_test.go @@ -147,10 +147,10 @@ func TestSearchRepositoryByName(t *testing.T) { count: 14}, {name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true}, - count: 15}, + count: 16}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, - count: 19}, + count: 20}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, count: 13}, @@ -159,7 +159,7 @@ func TestSearchRepositoryByName(t *testing.T) { count: 11}, {name: "AllPublic/PublicRepositoriesOfOrganization", opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse}, - count: 15}, + count: 16}, } for _, testCase := range testCases { diff --git a/models/repo_watch.go b/models/repo_watch.go index fb89a55a1..8019027c1 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -109,6 +109,23 @@ func notifyWatchers(e Engine, act *Action) error { act.ID = 0 act.UserID = watches[i].UserID + act.Repo.Units = nil + + switch act.OpType { + case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionDeleteBranch: + if !act.Repo.CheckUnitUser(act.UserID, false, UnitTypeCode) { + continue + } + case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue: + if !act.Repo.CheckUnitUser(act.UserID, false, UnitTypeIssues) { + continue + } + case ActionCreatePullRequest, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest: + if !act.Repo.CheckUnitUser(act.UserID, false, UnitTypePullRequests) { + continue + } + } + if _, err = e.InsertOne(act); err != nil { return fmt.Errorf("insert new action: %v", err) } diff --git a/models/topic.go b/models/topic.go index 3b1737f8a..247aac5ff 100644 --- a/models/topic.go +++ b/models/topic.go @@ -6,6 +6,7 @@ package models import ( "fmt" + "regexp" "strings" "code.gitea.io/gitea/modules/util" @@ -20,6 +21,8 @@ func init() { ) } +var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) + // Topic represents a topic of repositories type Topic struct { ID int64 @@ -51,6 +54,11 @@ func (err ErrTopicNotExist) Error() string { return fmt.Sprintf("topic is not exist [name: %s]", err.Name) } +// ValidateTopic checks topics by length and match pattern rules +func ValidateTopic(topic string) bool { + return len(topic) <= 35 && topicPattern.MatchString(topic) +} + // GetTopicByName retrieves topic by name func GetTopicByName(name string) (*Topic, error) { var topic Topic @@ -182,6 +190,13 @@ func SaveTopics(repoID int64, topicNames ...string) error { } } + topicNames = topicNames[:0] + if err := sess.Table("topic").Cols("name"). + Join("INNER", "repo_topic", "topic.id = repo_topic.topic_id"). + Where("repo_topic.repo_id = ?", repoID).Find(&topicNames); err != nil { + return err + } + if _, err := sess.ID(repoID).Cols("topics").Update(&Repository{ Topics: topicNames, }); err != nil { diff --git a/models/topic_test.go b/models/topic_test.go index 472f4e52d..ef374e557 100644 --- a/models/topic_test.go +++ b/models/topic_test.go @@ -55,3 +55,16 @@ func TestAddTopic(t *testing.T) { assert.NoError(t, err) assert.EqualValues(t, 2, len(topics)) } + +func TestTopicValidator(t *testing.T) { + assert.True(t, ValidateTopic("12345")) + assert.True(t, ValidateTopic("2-test")) + assert.True(t, ValidateTopic("test-3")) + assert.True(t, ValidateTopic("first")) + assert.True(t, ValidateTopic("second-test-topic")) + assert.True(t, ValidateTopic("third-project-topic-with-max-length")) + + assert.False(t, ValidateTopic("$fourth-test,topic")) + assert.False(t, ValidateTopic("-fifth-test-topic")) + assert.False(t, ValidateTopic("sixth-go-project-topic-with-excess-length")) +} diff --git a/models/user.go b/models/user.go index 1497eef44..653e99426 100644 --- a/models/user.go +++ b/models/user.go @@ -546,28 +546,46 @@ func (u *User) GetRepositories(page, pageSize int) (err error) { return err } -// GetRepositoryIDs returns repositories IDs where user owned -func (u *User) GetRepositoryIDs() ([]int64, error) { +// GetRepositoryIDs returns repositories IDs where user owned and has unittypes +func (u *User) GetRepositoryIDs(units ...UnitType) ([]int64, error) { var ids []int64 - return ids, x.Table("repository").Cols("id").Where("owner_id = ?", u.ID).Find(&ids) + + sess := x.Table("repository").Cols("repository.id") + + if len(units) > 0 { + sess = sess.Join("INNER", "repo_unit", "repository.id = repo_unit.repo_id") + sess = sess.In("repo_unit.type", units) + } + + return ids, sess.Where("owner_id = ?", u.ID).Find(&ids) } -// GetOrgRepositoryIDs returns repositories IDs where user's team owned -func (u *User) GetOrgRepositoryIDs() ([]int64, error) { +// GetOrgRepositoryIDs returns repositories IDs where user's team owned and has unittypes +func (u *User) GetOrgRepositoryIDs(units ...UnitType) ([]int64, error) { var ids []int64 - return ids, x.Table("repository"). + + sess := x.Table("repository"). Cols("repository.id"). - Join("INNER", "team_user", "repository.owner_id = team_user.org_id AND team_user.uid = ?", u.ID). + Join("INNER", "team_user", "repository.owner_id = team_user.org_id"). + Join("INNER", "team_repo", "repository.is_private != ? OR (team_user.team_id = team_repo.team_id AND repository.id = team_repo.repo_id)", true) + + if len(units) > 0 { + sess = sess.Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id") + sess = sess.In("team_unit.type", units) + } + + return ids, sess. + Where("team_user.uid = ?", u.ID). GroupBy("repository.id").Find(&ids) } // GetAccessRepoIDs returns all repositories IDs where user's or user is a team member organizations -func (u *User) GetAccessRepoIDs() ([]int64, error) { - ids, err := u.GetRepositoryIDs() +func (u *User) GetAccessRepoIDs(units ...UnitType) ([]int64, error) { + ids, err := u.GetRepositoryIDs(units...) if err != nil { return nil, err } - ids2, err := u.GetOrgRepositoryIDs() + ids2, err := u.GetOrgRepositoryIDs(units...) if err != nil { return nil, err } diff --git a/models/user_test.go b/models/user_test.go index 4fd0bc0fa..20de1a64b 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -159,3 +159,25 @@ func BenchmarkHashPassword(b *testing.B) { u.HashPassword(pass) } } + +func TestGetOrgRepositoryIDs(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + user2 := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) + user4 := AssertExistsAndLoadBean(t, &User{ID: 4}).(*User) + user5 := AssertExistsAndLoadBean(t, &User{ID: 5}).(*User) + + accessibleRepos, err := user2.GetOrgRepositoryIDs() + assert.NoError(t, err) + // User 2's team has access to private repos 3, 5, repo 32 is a public repo of the organization + assert.Equal(t, []int64{3, 5, 32}, accessibleRepos) + + accessibleRepos, err = user4.GetOrgRepositoryIDs() + assert.NoError(t, err) + // User 4's team has access to private repo 3, repo 32 is a public repo of the organization + assert.Equal(t, []int64{3, 32}, accessibleRepos) + + accessibleRepos, err = user5.GetOrgRepositoryIDs() + assert.NoError(t, err) + // User 5's team has no access to any repo + assert.Len(t, accessibleRepos, 0) +} diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 364308424..972b62261 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -83,12 +83,12 @@ host=Host user=Benutzername password=Passwort db_name=Datenbankname -db_helper=Hinweis für MySQL-Benutzer: Bitte verwende das InnoDB Speichersubsystem und den Zeichensatz "utf8_general_ci". +db_helper=Hinweis für MySQL-Benutzer: Bitte verwende das InnoDB-Speichersubsystem und den Zeichensatz „utf8_general_ci“. ssl_mode=SSL path=Pfad sqlite_helper=Der Dateipfad zur SQLite3- oder TiDB-Datenbank.
Bitte verwende einen absoluten Pfad, wenn Gitea als Service gestartet wird. -err_empty_db_path=Der SQLite3 oder TiDB Datenbankpfad darf nicht leer sein. -err_invalid_tidb_name=Der TiDB Datenbankname darf nicht die Zeichen "." und "-" enthalten. +err_empty_db_path=Der SQLite3- oder TiDB-Datenbankpfad darf nicht leer sein. +err_invalid_tidb_name=Der TiDB-Datenbankname darf nicht die Zeichen „.“ und „-“ enthalten. no_admin_and_disable_registration=Du kannst Selbst-Registrierungen nicht deaktivieren, ohne ein Administratorkonto zu erstellen. err_empty_admin_password=Das Administrator-Passwort darf nicht leer sein. @@ -97,17 +97,17 @@ app_name=Seitentitel app_name_helper=Du kannst hier den Namen deines Unternehmens eingeben. repo_path=Repository-Verzeichnis repo_path_helper=Remote-Git-Repositories werden in diesem Verzeichnis gespeichert. -lfs_path=Git LFS-Wurzelpfad +lfs_path=Git-LFS-Wurzelpfad lfs_path_helper=In diesem Verzeichnis werden die Dateien von Git LFS abgespeichert. Leer lassen um LFS zu deaktivieren. run_user=Ausführen als run_user_helper=Gebe den Betriebssystem-Benutzernamen ein, unter welchem Gitea laufen soll. Beachte, dass dieser Nutzer Zugriff auf den Repository-Ordner haben muss. -domain=SSH Server-Domain +domain=SSH-Server-Domain domain_helper=Domain oder Host-Adresse für die SSH-URL. -ssh_port=SSH Server Port +ssh_port=SSH-Server-Port ssh_port_helper=Der Port deines SSH-Servers. Leer lassen um SSH zu deaktivieren. -http_port=Gitea HTTP-Listen-Port -http_port_helper=Port unter dem der Gitea Web Server laufen soll. -app_url=Gitea Basis-URL +http_port=Gitea-HTTP-Listen-Port +http_port_helper=Port, unter dem der Gitea-Webserver laufen soll. +app_url=Gitea-Basis-URL app_url_helper=Adresse für HTTP(S)-Klon-URLs und E-Mail-Benachrichtigungen. log_root_path=Logdateipfad log_root_path_helper=Log-Dateien werden in diesem Verzeichnis gespeichert. @@ -117,8 +117,8 @@ email_title=E-Mail-Einstellungen smtp_host=SMTP-Server smtp_from=E-Mail senden als smtp_from_helper=E-Mail-Adresse, die von Gitea genutzt werden soll. Bitte gib die E-Mail-Adresse im '"Name" '-Format ein. -mailer_user=SMTP Benutzername -mailer_password=SMTP Passwort +mailer_user=SMTP-Benutzername +mailer_password=SMTP-Passwort register_confirm=E-Mail-Bestätigung benötigt zum Registrieren mail_notify=E-Mail-Benachrichtigungen aktivieren server_service_title=Sonstige Server- und Drittserviceeinstellungen @@ -131,9 +131,9 @@ federated_avatar_lookup_popup=Föderierte Profilbilder via Libravatar aktivieren disable_registration=Registrierung deaktivieren disable_registration_popup=Registrierung neuer Benutzer deaktivieren. Nur Administratoren werden neue Benutzerkonten anlegen können. allow_only_external_registration_popup=Registrierung nur über externe Services aktiveren. -openid_signin=OpenID Anmeldung aktivieren +openid_signin=OpenID-Anmeldung aktivieren openid_signin_popup=Benutzeranmeldung via OpenID aktivieren. -openid_signup=OpenID Selbstregistrierung aktivieren +openid_signup=OpenID-Selbstregistrierung aktivieren openid_signup_popup=OpenID-basierte Selbstregistrierung aktivieren. enable_captcha=CAPTCHA aktivieren enable_captcha_popup=Captcha-Eingabe bei der Registrierung erforderlich. @@ -147,10 +147,10 @@ confirm_password=Passwort bestätigen admin_email=E-Mail-Adresse install_btn_confirm=Gitea installieren test_git_failed=Fehler beim Test des 'git' Kommandos: %v -sqlite3_not_available=Diese Gitea-Version unterstützt SQLite3 nicht. Bitte lade die offizielle binäre Version von %s herunter (nicht die 'gobuild'-Version). +sqlite3_not_available=Diese Gitea-Version unterstützt SQLite3 nicht. Bitte lade die offizielle binäre Version von %s herunter (nicht die „gobuild“-Version). invalid_db_setting=Datenbankeinstellungen sind ungültig: %v invalid_repo_path=Repository-Verzeichnis ist ungültig: %v -run_user_not_match=Der "Ausführen als" Benutzer ist nicht der aktuelle Benutzer: %s -> %s +run_user_not_match=Der „Ausführen als“-Benutzername ist nicht der aktuelle Benutzername: %s -> %s save_config_failed=Fehler beim Speichern der Konfiguration: %v invalid_admin_setting=Administrator-Konto Einstellungen sind ungültig: %v install_success=Willkommen! Danke, dass du Gitea gewählt hast. Viel Spaß! @@ -162,7 +162,7 @@ default_allow_create_organization_popup=Neuen Nutzern das Erstellen von Organisa default_enable_timetracking=Zeiterfassung standardmäßig aktivieren default_enable_timetracking_popup=Zeiterfassung standardmäßig für neue Repositories aktivieren. no_reply_address=Versteckte E-Mail-Domain -no_reply_address_helper=Domain-Namen für Benutzer mit einer versteckten Emailadresse. Zum Beispiel wird der Benutzername "Joe" in Git als "joe@noreply.example.org" protokolliert, wenn die versteckte E-Mail-Domäne "noreply.example.org" festgelegt ist. +no_reply_address_helper=Domain-Name für Benutzer mit einer versteckten Emailadresse. Zum Beispiel wird der Benutzername „Joe“ in Git als „joe@noreply.example.org“ protokolliert, wenn die versteckte E-Mail-Domain „noreply.example.org“ festgelegt ist. [home] uname_holder=E-Mail-Adresse oder Benutzername @@ -225,9 +225,9 @@ login_userpass=Anmelden login_openid=OpenID openid_connect_submit=Verbinden openid_connect_title=Mit bestehendem Konto verbinden -openid_connect_desc=Die gewählte OpenID URI ist unbekannt. Ordne sie hier einem neuen Account zu. +openid_connect_desc=Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu. openid_register_title=Neues Konto einrichten -openid_register_desc=Die gewählte OpenID URI ist unbekannt. Ordne sie hier einem neuen Account zu. +openid_register_desc=Die gewählte OpenID-URI ist unbekannt. Ordne sie hier einem neuen Account zu. openid_signin_desc=Gib deine OpenID-URI ein. Zum Beispiel: https://anne.me, bob.openid.org.cn oder gnusocial.net/carry. disable_forgot_password_mail=Das Zurücksetzen von Passwörtern wurde deaktiviert. Bitte wende dich an den Administrator. @@ -264,8 +264,8 @@ TreeName=Dateipfad Content=Inhalt require_error=` darf nicht leer sein.` -alpha_dash_error=` sollte nur Buchstaben, Zahlen, Bindestriche ('-') und Unterstriche ('_') enthalten` -alpha_dash_dot_error=` sollte nur Buchstaben, Zahlen, Bindestriche ('-'), Unterstriche ('_') und Punkte ('.') enthalten` +alpha_dash_error=` sollte nur Buchstaben, Zahlen, Bindestriche („-“) und Unterstriche („_“) enthalten.` +alpha_dash_dot_error=` sollte nur Buchstaben, Zahlen, Bindestriche („-“), Unterstriche („_“) und Punkte („.“) enthalten.` git_ref_name_error=` muss ein wohlgeformter Git-Referenzname sein.` size_error=` muss die Größe %s haben.` min_size_error=` muss mindestens %s Zeichen enthalten.` @@ -283,13 +283,13 @@ org_name_been_taken=Der Organisationsname ist bereits vergeben. team_name_been_taken=Der Teamname ist bereits vergeben. team_no_units_error=Das Team muss auf mindestens einen Bereich Zugriff haben. email_been_used=Die E-Mail-Adresse wird bereits verwendet. -openid_been_used=Die OpenID-Adresse "%s" wird bereits verwendet. +openid_been_used=Die OpenID-Adresse „%s“ wird bereits verwendet. username_password_incorrect=Benutzername oder Passwort ist falsch. enterred_invalid_repo_name=Der eingegebenen Repository-Name ist falsch. -enterred_invalid_owner_name=Der Name des neuen Besitzers ist invalid. +enterred_invalid_owner_name=Der Name des neuen Besitzers ist ungültig. enterred_invalid_password=Das eingegebene Passwort ist falsch. user_not_exist=Dieser Benutzer ist nicht vorhanden. -last_org_owner=Du kannst den letzten Benutzer nicht aus dem "Besitzer"-Team entferenen. Es muss mindestens ein Besitzer in einer Organisation geben. +last_org_owner=Du kannst den letzten Benutzer nicht aus dem „Besitzer“-Team entfernen. Es muss mindestens einen Besitzer in einer Organisation geben. cannot_add_org_to_team=Eine Organisation kann nicht als Teammitglied hinzugefügt werden. invalid_ssh_key=Dein SSH-Key kann nicht überprüft werden: %s @@ -349,7 +349,7 @@ continue=Weiter cancel=Abbrechen language=Sprache -lookup_avatar_by_mail=Avatar anhand der E-Mail-Addresse suchen +lookup_avatar_by_mail=Profilbild anhand der E-Mail-Addresse suchen federated_avatar_lookup=Suche nach föderierten Profilbildern enable_custom_avatar=Benutzerdefiniertes Profilbild benutzen choose_new_avatar=Neues Profilbild auswählen @@ -364,7 +364,7 @@ new_password=Neues Passwort retype_new_password=Neues Passwort erneut eingeben password_incorrect=Das aktuelle Passwort ist falsch. change_password_success=Dein Passwort wurde aktualisiert. Bitte verwende dieses beim nächsten Einloggen. -password_change_disabled=Benutzer, die nicht von Gitea verwaltet werden, können ihr Passwort im Web Interface nicht ändern. +password_change_disabled=Benutzer, die nicht von Gitea verwaltet werden, können ihr Passwort im Web-Interface nicht ändern. emails=E-Mail-Adressen manage_emails=E-Mail-Adressen verwalten @@ -383,7 +383,7 @@ add_new_email=Neue E-Mail-Adresse hinzufügen add_new_openid=Neue OpenID-URI hinzufügen add_email=E-Mail-Adresse hinzufügen add_openid=OpenID-URI hinzufügen -add_email_confirmation_sent=Eine Bestätigungs-E-Mail wurde an '%s' gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die E-Mail-Adresse zu bestätigen. +add_email_confirmation_sent=Eine Bestätigungs-E-Mail wurde an „%s“ gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die E-Mail-Adresse zu bestätigen. add_email_success=Die neue E-Mail-Addresse wurde hinzugefügt. add_openid_success=Die neue OpenID-Adresse wurde hinzugefügt. keep_email_private=E-Mail-Adresse verbergen @@ -394,8 +394,8 @@ manage_ssh_keys=SSH-Schlüssel verwalten manage_gpg_keys=GPG-Schlüssel verwalten add_key=Schlüssel hinzufügen ssh_desc=Diese öffentlichen SSH-Keys sind mit deinem Account verbunden. Der dazugehörigen privaten SSH-Keys geben dir vollen Zugriff auf deine Repositories. -gpg_desc=Diese öffentlichen GPG-Keys sind mit deinem Account verbunden. Halte die dazugehörigen privaten SSH-Keys geheim, da diese deine Commits signieren. -ssh_helper=Brauchst du Hilfe? Hier ist Githubs Anleitung zum Erzeugen von SSH-Schlüsseln oder Lösen einfacher SSH-Probleme. +gpg_desc=Diese öffentlichen GPG-Keys sind mit deinem Account verbunden. Halte die dazugehörigen privaten GPG-Keys geheim, da diese deine Commits signieren. +ssh_helper=Brauchst du Hilfe? Hier ist GitHubs Anleitung zum Erzeugen von SSH-Schlüsseln oder zum Lösen einfacher SSH-Probleme. gpg_helper=Brauchst du Hilfe? Hier ist GitHubs Anleitung über GPG. add_new_key=SSH-Schlüssel hinzufügen add_new_gpg_key=GPG-Schlüssel hinzufügen @@ -407,8 +407,8 @@ subkeys=Unterschlüssel key_id=Schlüssel-ID key_name=Schlüsselname key_content=Inhalt -add_key_success=Der SSH-Schlüssel "%s" wurde hinzugefügt. -add_gpg_key_success=Der GPG-Key "%s" wurde hinzugefügt. +add_key_success=Der SSH-Schlüssel „%s“ wurde hinzugefügt. +add_gpg_key_success=Der GPG-Key „%s“ wurde hinzugefügt. delete_key=Entfernen ssh_key_deletion=SSH-Schlüssel entfernen gpg_key_deletion=GPG-Schlüssel entfernen @@ -511,10 +511,10 @@ create_repo=Repository erstellen default_branch=Standardbranch mirror_prune=Entfernen mirror_prune_desc=Entferne veraltete remote-tracking Referenzen -mirror_interval=Spiegelintervall (gültige Zeiteinheiten sind 'h', 'm', 's') +mirror_interval=Spiegelintervall (gültige Zeiteinheiten sind „h“, „m“, „s“) mirror_interval_invalid=Das Spiegel-Intervall ist ungültig. mirror_address=Klonen via URL -mirror_address_desc=Bitte gebe alle benötigten Zugangsdaten in der URL an. +mirror_address_desc=Bitte gib alle benötigten Zugangsdaten in der URL an. mirror_last_synced=Zuletzt synchronisiert watchers=Beobachter stargazers=Favorisiert von @@ -523,7 +523,7 @@ pick_reaction=Wähle eine Reaktion reactions_more=und %d weitere form.reach_limit_of_creation=Du hast bereits dein Limit von %d Repositories erreicht. -form.name_reserved=Der Repository-Name '%s' ist reserviert. +form.name_reserved=Der Repository-Name „%s“ ist reserviert. form.name_pattern_not_allowed='%s' ist nicht erlaubt für Repository-Namen. need_auth=Authentifizierung zum Klonen benötigt @@ -588,7 +588,7 @@ editor.edit_file=Datei bearbeiten editor.preview_changes=Vorschau der Änderungen editor.cannot_edit_non_text_files=Binärdateien können nicht im Webinterface bearbeitet werden. editor.edit_this_file=Datei bearbeiten -editor.must_be_on_a_branch=Du musst dich in einer Branch befinden, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. +editor.must_be_on_a_branch=Du musst dich in einem Branch befinden, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. editor.fork_before_edit=Du musst dieses Repository forken, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. editor.delete_this_file=Datei löschen editor.must_have_write_access=Du benötigst Schreibzugriff, um Änderungen an dieser Datei vorzuschlagen oder vorzunehmen. @@ -598,31 +598,31 @@ editor.filename_help=Füge einen Ordner hinzu, indem du seinen Namen und anschli editor.or=oder editor.cancel_lower=Abbrechen editor.commit_changes=Änderungen committen -editor.add_tmpl='%s/' hinzufügen -editor.add='%s' hinzufügen -editor.update='%s' ändern -editor.delete='%s' löschen +editor.add_tmpl=„%s/“ hinzufügen +editor.add=„%s“ hinzufügen +editor.update=„%s“ ändern +editor.delete=„%s“ löschen editor.commit_message_desc=Eine ausführlichere (optionale) Beschreibung hinzufügen… -editor.commit_directly_to_this_branch=Direkt in die %s-Branch einchecken. -editor.create_new_branch=Einen neue Branch für diesen Commit erstellen und einen Pull Request starten. +editor.commit_directly_to_this_branch=Direkt in den Branch „%s“ einchecken. +editor.create_new_branch=Einen neuen Branch für diesen Commit erstellen und einen Pull Request starten. editor.new_branch_name_desc=Neuer Branchname… editor.cancel=Abbrechen editor.filename_cannot_be_empty=Der Dateiname darf nicht leer sein. editor.branch_already_exists=Branch '%s' existiert bereits in diesem Repository. -editor.directory_is_a_file=Der Verzeichnisname '%s' wird bereits als Dateiname in diesem Repository verwendet. +editor.directory_is_a_file=Der Verzeichnisname „%s“ wird bereits als Dateiname in diesem Repository verwendet. editor.file_is_a_symlink='%s' ist ein symolischer Link. Symbolische Links können mit dem Web Editor nicht bearbeitet werden. -editor.filename_is_a_directory=Der Dateiname '%s' wird bereits als Verzeichnisname in diesem Repository verwendet. -editor.file_editing_no_longer_exists=Die bearbeitete Datei '%s' existiert nicht mehr in diesem Repository. -editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. Hier klicken um die Änderungen anzusehen, oder Änderungen erneut comitten um sie zu überschreiben. -editor.file_already_exists=Eine Datei mit dem Namen '%s' ist bereits in diesem Repository vorhanden. +editor.filename_is_a_directory=Der Dateiname „%s“ wird bereits als Verzeichnisname in diesem Repository verwendet. +editor.file_editing_no_longer_exists=Die bearbeitete Datei „%s“ existiert nicht mehr in diesem Repository. +editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. Hier klicken, um die Änderungen anzusehen, oder Änderungen erneut comitten, um sie zu überschreiben. +editor.file_already_exists=Eine Datei mit dem Namen „%s“ ist bereits in diesem Repository vorhanden. editor.no_changes_to_show=Keine Änderungen vorhanden. editor.fail_to_update_file=Fehler beim Ändern/Erstellen der Datei '%s'. Fehler: %v editor.add_subdir=Verzeichnis erstellen… -editor.unable_to_upload_files=Fehler beim Hochladen der Dateien nach '%s'. Fehler: %v +editor.unable_to_upload_files=Fehler beim Hochladen der Dateien nach „%s“. Fehler: %v editor.upload_files_to_dir=Dateien hochladen nach '%s' -editor.cannot_commit_to_protected_branch=Commit in den geschützten Branch '%s' ist nicht möglich. +editor.cannot_commit_to_protected_branch=Commit in den geschützten Branch „%s“ ist nicht möglich. -commits.desc=Durchsuche die Quellcode Änderungshistorie. +commits.desc=Durchsuche die Quellcode-Änderungshistorie. commits.commits=Commits commits.search=Commits durchsuchen… commits.find=Suchen @@ -633,7 +633,7 @@ commits.date=Datum commits.older=Älter commits.newer=Neuer commits.signed_by=Signiert von -commits.gpg_key_id=GPG Schlüssel-ID +commits.gpg_key_id=GPG-Schlüssel-ID ext_issues=Externe Issues ext_issues.desc=Link zu externem Issuetracker. @@ -658,7 +658,7 @@ issues.new_label_placeholder=Labelname issues.new_label_desc_placeholder=Beschreibung issues.create_label=Label erstellen issues.label_templates.title=Lade vordefinierte Label -issues.label_templates.info=Es existieren noch keine Labels. Erstelle ein neues Label ("Neues Label") oder verwende das Standard Label-Set: +issues.label_templates.info=Es existieren noch keine Label. Erstelle ein neues Label („Neues Label“) oder verwende das Standard-Label-Set: issues.label_templates.helper=Wähle ein Label issues.label_templates.use=Label-Set verwenden issues.label_templates.fail_to_load_file=Fehler beim Laden der Label Template Datei '%s': %v @@ -676,7 +676,7 @@ issues.delete_branch_at=`löschte die Branch %s %s` issues.open_tab=%d offen issues.close_tab=%d geschlossen issues.filter_label=Label -issues.filter_label_no_select=Alle Labels +issues.filter_label_no_select=Alle Label issues.filter_milestone=Meilenstein issues.filter_milestone_no_select=Alle Meilensteine issues.filter_assignee=Zuständig @@ -768,10 +768,10 @@ issues.cancel_tracking_history=hat die Zeiterfassung %s abgebrochen issues.time_spent_total=Zeitaufwand insgesamt issues.time_spent_from_all_authors=`Aufgewendete Zeit: %s` issues.due_date=Fällig am -issues.invalid_due_date_format=Das Fälligkeitsdatum muss das Format 'JJJJ-MM-TT' haben. +issues.invalid_due_date_format=Das Fälligkeitsdatum muss das Format „JJJJ-MM-TT“ haben. issues.error_modifying_due_date=Fehler beim Ändern des Fälligkeitsdatums. issues.error_removing_due_date=Fehler beim Entfernen des Fälligkeitsdatums. -issues.due_date_form=jjjj-mm-tt +issues.due_date_form=JJJJ-MM-TT issues.due_date_form_add=Fälligkeitsdatum hinzufügen issues.due_date_form_update=Fälligkeitsdatum ändern issues.due_date_form_remove=Fälligkeitsdatum löschen @@ -787,7 +787,7 @@ pulls.new=Neuer Pull-Request pulls.compare_changes=Neuer Pull-Request pulls.compare_changes_desc=Wähle die Ziel- und Quellbranch aus. pulls.compare_base=Ziel -pulls.compare_compare=pull von +pulls.compare_compare=pullen von pulls.filter_branch=Branch filtern pulls.no_results=Keine Ergebnisse verfügbar. pulls.nothing_to_compare=Diese Branches sind identisch. Es muss kein Pull-Request erstellt werden. @@ -827,13 +827,13 @@ milestones.title=Titel milestones.desc=Beschreibung milestones.due_date=Fälligkeitsdatum (optional) milestones.clear=Feld leeren -milestones.invalid_due_date_format=Das Fälligkeitsdatum muss das Format 'JJJJ-MM-TT' haben. -milestones.create_success=Der Meilenstein '%s' wurde erstellt. +milestones.invalid_due_date_format=Das Fälligkeitsdatum muss das Format „JJJJ-MM-TT“ haben. +milestones.create_success=Der Meilenstein „%s“ wurde erstellt. milestones.edit=Meilenstein bearbeiten milestones.edit_subheader=Benutze Meilensteine, um Issues zu organisieren und den Fortschritt darzustellen. milestones.cancel=Abbrechen milestones.modify=Meilenstein bearbeiten -milestones.edit_success=Die Änderungen am Meilenstein "%s" wurden gespeichert. +milestones.edit_success=Die Änderungen am Meilenstein „%s“ wurden gespeichert. milestones.deletion=Meilenstein löschen milestones.deletion_desc=Das Löschen des Meilensteins entfernt ihn von allen Issues. Fortfahren? milestones.deletion_success=Der Meilenstein wurde gelöscht. @@ -849,7 +849,7 @@ ext_wiki.desc=Verweis auf externes Wiki. wiki=Wiki wiki.welcome=Willkommen im Wiki. -wiki.welcome_desc=Im Wiki kannst Dokumentation schreiben und mit Mitarbeitern teilen. +wiki.welcome_desc=Im Wiki kannst du Dokumentation schreiben und sie mit Mitarbeitern teilen. wiki.desc=Schreibe und teile Dokumentation mit Mitarbeitern. wiki.create_first_page=Erstelle die erste Seite wiki.page=Seite @@ -861,9 +861,9 @@ wiki.last_commit_info=%s hat diese Seite bearbeitet %s wiki.edit_page_button=Bearbeiten wiki.new_page_button=Neue Seite wiki.delete_page_button=Seite löschen -wiki.delete_page_notice_1=Das Löschen der Wiki-Seite '%s' kann nicht Rückgängig gemacht werden. Fortfahren? +wiki.delete_page_notice_1=Das Löschen der Wiki-Seite „%s“ kann nicht rückgängig gemacht werden. Fortfahren? wiki.page_already_exists=Eine Wiki-Seite mit dem gleichen Namen existiert bereits. -wiki.reserved_page=Der Wiki-Seitenname "%s" ist reserviert. +wiki.reserved_page=Der Wiki-Seitenname „%s“ ist reserviert. wiki.pages=Seiten wiki.last_updated=Zuletzt aktualisiert %s @@ -911,7 +911,7 @@ activity.published_release_label=Veröffentlicht search=Suchen search.search_repo=Repository durchsuchen -search.results=Suchergebnisse für "%s" in %s +search.results=Suchergebnisse für „%s“ in %s settings=Einstellungen settings.desc=In den Einstellungen kannst du die Einstellungen des Repository anpassen @@ -925,28 +925,28 @@ settings.hooks=Webhooks settings.githooks=Git-Hooks settings.basic_settings=Grundeinstellungen settings.mirror_settings=Mirror Einstellungen -settings.sync_mirror=Jetzt Synchronisieren +settings.sync_mirror=Jetzt synchronisieren settings.mirror_sync_in_progress=Mirror-Synchronisierung wird zurzeit ausgeführt. Komm in ein paar Minuten zurück. settings.site=Webseite settings.update_settings=Einstellungen speichern settings.advanced_settings=Erweiterte Einstellungen -settings.wiki_desc=Repository Wiki aktivieren +settings.wiki_desc=Repository-Wiki aktivieren settings.use_internal_wiki=Eingebautes Wiki verwenden settings.use_external_wiki=Externes Wiki verwenden settings.external_wiki_url=Externe Wiki URL settings.external_wiki_url_error=Die externe Wiki-URL ist ungültig. -settings.external_wiki_url_desc=Besucher werden auf die externe Wiki-URL weitergeleitet wenn sie auf das Wiki-Tab klicken. -settings.issues_desc=Repository Issue-Tracker aktivieren +settings.external_wiki_url_desc=Besucher werden auf die externe Wiki-URL weitergeleitet, wenn sie auf das Wiki-Tab klicken. +settings.issues_desc=Repository-Issue-Tracker aktivieren settings.use_internal_issue_tracker=Integrierten Issue-Tracker verwenden settings.use_external_issue_tracker=Externen Issue-Tracker verwenden settings.external_tracker_url=URL eines externen Issue Trackers settings.external_tracker_url_error=Die URL des externen Issue-Trackers ist ungültig. -settings.external_tracker_url_desc=Besucher werden auf die externe Issue-Tracker-URL weitergeleitet wenn sie auf das Issues-Tab klicken. +settings.external_tracker_url_desc=Besucher werden auf die externe Issue-Tracker-URL weitergeleitet, wenn sie auf das Issues-Tab klicken. settings.tracker_url_format=URL-Format des externen Issue-Systems settings.tracker_issue_style=Namenskonvention des externen Issue-Trackers settings.tracker_issue_style.numeric=Numerisch settings.tracker_issue_style.alphanumeric=Alphanumerisch -settings.tracker_url_format_desc=Du kannst die Platzhalter {user}, {repo}, {index} für den Benutzernamen, den Namen des Repositories und die Issue-Nummer verwenden. +settings.tracker_url_format_desc=Du kannst die Platzhalter {user}, {repo}, {index} für den Benutzernamen, den Namen des Repositorys und die Issue-Nummer verwenden. settings.enable_timetracker=Zeiterfassung aktivieren settings.allow_only_contributors_to_track_time=Nur Mitarbeitern erlauben, die Zeiterfassung zu nutzen settings.pulls_desc=Repository-Pull-Requests aktivieren @@ -964,22 +964,22 @@ settings.convert_notices_1=Dieser Vorgang wandelt das Mirror-Repository in ein n settings.convert_confirm=Repository umwandeln settings.convert_succeed=Das Mirror-Repository wurde erfolgreich in ein normales Repository umgewandelt. settings.transfer=Besitz übertragen -settings.transfer_desc=Übertrage dieses Repository auf einen anderen Benutzer oder eine Organisation in der Du Admin-Rechte hast. -settings.transfer_notices_1=- Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist. -settings.transfer_notices_2=- Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist. +settings.transfer_desc=Übertrage dieses Repository auf einen anderen Benutzer oder eine Organisation, in der du Admin-Rechte hast. +settings.transfer_notices_1=– Du wirst keinen Zugriff mehr haben, wenn der neue Besitzer ein individueller Benutzer ist. +settings.transfer_notices_2=– Du wirst weiterhin Zugriff haben, wenn der neue Besitzer eine Organisation ist und du einer der Besitzer bist. settings.transfer_form_title=Gib den Repository-Namen zur Bestätigung ein: settings.wiki_delete=Wiki-Daten löschen settings.wiki_delete_desc=Das Löschen von Wiki-Daten kann nicht rückgängig gemacht werden. Bitte sei vorsichtig. -settings.wiki_delete_notices_1=- Dies löscht und deaktiviert das Wiki für %s. +settings.wiki_delete_notices_1=– Dies löscht und deaktiviert das Wiki für %s. settings.confirm_wiki_delete=Wiki-Daten löschen -settings.wiki_deletion_success=Repository Wiki-Daten wurden gelöscht. +settings.wiki_deletion_success=Repository-Wiki-Daten wurden gelöscht. settings.delete=Dieses Repository löschen settings.delete_desc=Wenn dieses Repository gelöscht wurde, gibt es keinen Weg zurück. Bitte sei vorsichtig. settings.delete_notices_1=- Diese Operation kann NICHT rückgängig gemacht werden. -settings.delete_notices_2=- Die Operation wird das %s-Repository dauerhaft löschen, inklusive der Dateien, Issues, Kommentare und Zugriffseinstellungen. -settings.delete_notices_fork_1=- Nach dem Löschen werden alle Forks unabhängig. +settings.delete_notices_2=– Die Operation wird das %s-Repository dauerhaft löschen, inklusive der Dateien, Issues, Kommentare und Zugriffseinstellungen. +settings.delete_notices_fork_1=– Forks dieses Repositorys werden nach dem Löschen unabhängig. settings.deletion_success=Das Repository wurde gelöscht. -settings.update_settings_success=Repository Einstellungen wurden aktualisiert. +settings.update_settings_success=Repository-Einstellungen wurden aktualisiert. settings.transfer_owner=Neuer Besitzer settings.make_transfer=Transfer durchführen settings.transfer_succeed=Das Repository wurde transferiert. @@ -994,7 +994,7 @@ settings.search_user_placeholder=Benutzer suchen… settings.org_not_allowed_to_be_collaborator=Organisationen können nicht als Mitarbeiter hinzugefügt werden. settings.user_is_org_member=Der Benutzer ist ein Organisationsmitglied und kann nicht als Mitarbeiter hinzugefügt werden. settings.add_webhook=Webhook hinzufügen -settings.hooks_desc=Webhooks senden bei bestimmten Gitea-Events automatisch HTTP POST-Requets an einen Server. Lies mehr in unserer Anleitung zu Webhooks (Englisch). +settings.hooks_desc=Webhooks senden bei bestimmten Gitea-Events automatisch „HTTP POST“-Anfragen an einen Server. Lies mehr in unserer Anleitung zu Webhooks (auf Englisch). settings.webhook_deletion=Webhook löschen settings.webhook_deletion_desc=Das Entfernen eines Webhooks löscht seine Einstellungen und Zustellungsverlauf. Fortfahren? settings.webhook_deletion_success=Webhook wurde entfernt. @@ -1066,18 +1066,18 @@ settings.title=Titel settings.deploy_key_content=Inhalt settings.key_been_used=Ein Deploy-Key mit identischem Inhalt wird bereits verwendet. settings.key_name_used=Ein Deploy-Key mit diesem Namen existiert bereits. -settings.add_key_success=Der Deploy-Key '%s' wurde erfolgreich hinzugefügt. +settings.add_key_success=Der Deploy-Key „%s“ wurde erfolgreich hinzugefügt. settings.deploy_key_deletion=Deploy-Key löschen settings.deploy_key_deletion_desc=Nach dem Löschen wird dieser Deploy-Key keinen Zugriff mehr auf dieses Repository haben. Fortfahren? settings.deploy_key_deletion_success=Der Deploy-Key wurde entfernt. settings.branches=Branches -settings.protected_branch=Branch-Protection +settings.protected_branch=Branch-Schutz settings.protected_branch_can_push=Push erlauben? settings.protected_branch_can_push_yes=Du kannst pushen settings.protected_branch_can_push_no=Du kannst nicht pushen -settings.branch_protection=Branch-Schutz" für Branch '%s' +settings.branch_protection=Branch-Schutz für Branch „%s“ settings.protect_this_branch=Brach-Schutz aktivieren -settings.protect_this_branch_desc=Verhindere Löschen und deaktiviere Git force push auf diese Branch. +settings.protect_this_branch_desc=Verhindere Löschen und deaktiviere das sog. „force pushing” von Git auf diesen Branch. settings.protect_whitelist_committers=Push-Whitelist aktivieren settings.protect_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Push-Beschränkungen zu umgehen. settings.protect_whitelist_users=Nutzer, die pushen dürfen: @@ -1085,17 +1085,17 @@ settings.protect_whitelist_search_users=Benutzer suchen… settings.protect_whitelist_teams=Teams, die pushen dürfen: settings.protect_whitelist_search_teams=Suche nach Teams… settings.protect_merge_whitelist_committers=Merge-Whitelist aktivieren -settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diese Branch zu mergen. +settings.protect_merge_whitelist_committers_desc=Erlaube Nutzern oder Teams auf der Whitelist Pull-Requests in diesen Branch zu mergen. settings.protect_merge_whitelist_users=Nutzer, die mergen dürfen: settings.protect_merge_whitelist_teams=Teams, die mergen dürfen: settings.add_protected_branch=Schutz aktivieren settings.delete_protected_branch=Schutz deaktivieren -settings.update_protect_branch_success=Branch-protection für die Branch '%s' wurde geändert. -settings.remove_protected_branch_success=Branch-protection für die Branch '%s' wurde deaktiviert. -settings.protected_branch_deletion=Brach-Schutz deaktivieren -settings.protected_branch_deletion_desc=Wenn du die Branch-Protection deaktivierst, können alle Nutzer mit Schreibrechten auf die Branch pushen. Fortfahren? -settings.default_branch_desc=Wähle eine Standardbranch für Pull-Requests und Code-Commits: -settings.choose_branch=Wähle eine Branch… +settings.update_protect_branch_success=Branch-Schutz für den Branch „%s“ wurde geändert. +settings.remove_protected_branch_success=Branch-Schutz für den Branch „%s“ wurde deaktiviert. +settings.protected_branch_deletion=Branch-Schutz deaktivieren +settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, können alle Nutzer mit Schreibrechten auf den Branch pushen. Fortfahren? +settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: +settings.choose_branch=Wähle einen Branch … settings.no_protected_branch=Es gibt keine geschützten Branches. diff.browse_source=Quellcode durchsuchen @@ -1131,7 +1131,7 @@ release.write=Schreiben release.preview=Vorschau release.loading=Laden… release.prerelease_desc=Als Pre-Release kennzeichnen -release.prerelease_helper=Dieses Release als "ungeeignet für den produktiven Einsatz" markieren. +release.prerelease_helper=Dieses Release als „ungeeignet für den produktiven Einsatz“ markieren. release.cancel=Abbrechen release.publish=Release veröffentlichen release.save_draft=Entwurf speichern @@ -1146,27 +1146,29 @@ release.downloads=Downloads branch.name=Branchname branch.search=Branches durchsuchen -branch.already_exists=Eine Branch mit dem Namen '%s' existiert bereits. +branch.already_exists=Ein Branch mit dem Namen „%s“ existiert bereits. branch.delete_head=Löschen -branch.delete=Branch '%s' löschen +branch.delete=Branch „%s“ löschen branch.delete_html=Branch löschen -branch.delete_desc=Das Löschen einer Branch ist permanent. Es KANN NICHT Rückgängig gemacht werden. Fortfahren? -branch.deletion_success=Branch '%s' wurde gelöscht. -branch.deletion_failed=Branch '%s' konnte nicht gelöscht werden. -branch.delete_branch_has_new_commits=Die Branch '%s' kann nicht gelöscht weden, da seit dem letzten Merge neue Commits hinzugefügt wurden. +branch.delete_desc=Das Löschen eines Branches ist permanent. Es KANN NICHT rückgängig gemacht werden. Fortfahren? +branch.deletion_success=Branch „%s“ wurde gelöscht. +branch.deletion_failed=Branch „%s“ konnte nicht gelöscht werden. +branch.delete_branch_has_new_commits=Der Branch „%s“ kann nicht gelöscht weden, da seit dem letzten Merge neue Commits hinzugefügt wurden. branch.create_branch=Erstelle Branch %s branch.create_from=von '%s' -branch.create_success=Branch '%s' wurde erstellt. +branch.create_success=Branch „%s“ wurde erstellt. branch.branch_already_exists=Branch '%s' existiert bereits in diesem Repository. -branch.branch_name_conflict=Der Branch-Name '%s' steht in Konflikt mit der bestehendem Branch '%s'. -branch.tag_collision=Branch '%s' kann nicht erstellt werden, da in diesem Repository bereits ein Tag mit dem selben Namen existiert. +branch.branch_name_conflict=Der Branch-Name „%s“ steht in Konflikt mit dem bestehenden Branch „%s“. +branch.tag_collision=Branch „%s“ kann nicht erstellt werden, da in diesem Repository bereits ein Tag mit dem selben Namen existiert. branch.deleted_by=Von %s gelöscht -branch.restore_success=Branch '%s' wurde wiederhergestellt. -branch.restore_failed=Wiederherstellung der Branch '%s' fehlgeschlagen. -branch.protected_deletion_failed=Branch '%s' ist geschützt und kann nicht gelöscht werden. +branch.restore_success=Branch „%s“ wurde wiederhergestellt. +branch.restore_failed=Wiederherstellung des Branches „%s“ fehlgeschlagen. +branch.protected_deletion_failed=Branch „%s“ ist geschützt und kann nicht gelöscht werden. topic.manage_topics=Themen verwalten topic.done=Fertig +topic.count_prompt=Du kannst nicht mehr als 25 Themen auswählen +topic.format_prompt=Themen müssen mit einem Buchstaben oder einer Zahl beginnen. Sie können Bindestriche (-) enthalten und dürfen nicht länger als 35 Zeichen sein [org] org_name_holder=Name der Organisation @@ -1188,9 +1190,9 @@ team_desc_helper=Beschreibe den Zweck oder die Rolle des Teams. team_permission_desc=Berechtigungen team_unit_desc=Zugriff auf Repositorybereiche erlauben -form.name_reserved=Der Organisationsname '%s' ist reserviert. -form.name_pattern_not_allowed=Das Muster '%s' ist in Organisationsnamen nicht erlaubt. -form.create_org_not_allowed=Du bist nicht berechtigt eine Organisation zu erstellen. +form.name_reserved=Der Organisationsname „%s“ ist reserviert. +form.name_pattern_not_allowed=Das Muster „%s“ ist in Organisationsnamen nicht erlaubt. +form.create_org_not_allowed=Du bist nicht berechtigt, eine Organisation zu erstellen. settings=Einstellungen settings.options=Organisation @@ -1229,7 +1231,7 @@ teams.read_access_helper=Mitglieder können Teamrepositories ansehen und klonen. teams.write_access=Schreibzugriff teams.write_access_helper=Mitglieder können Teamrepositories ansehen und auf sie pushen. teams.admin_access=Administratorzugang -teams.admin_access_helper=Mitglieder können auf Team Repositories "pushen", von ihnen "pullen" und Mitarbeiter hinzufügen. +teams.admin_access_helper=Mitglieder können auf Team-Repositorys pushen, von ihnen pullen und Mitarbeiter hinzufügen. teams.no_desc=Dieses Team hat keine Beschreibung teams.settings=Einstellungen teams.owners_permission_desc=Besitzer haben vollen Zugriff auf alle Repositories und Admin-Rechte für diese Organisation. @@ -1238,7 +1240,7 @@ teams.update_settings=Einstellungen aktualisieren teams.delete_team=Team löschen teams.add_team_member=Teammitglied hinzufügen teams.delete_team_title=Team löschen -teams.delete_team_desc=Das Löschen eines Teams wiederruft den Repository-Zugriff für seine Mitglieder. Fortfahren? +teams.delete_team_desc=Das Löschen eines Teams widerruft den Repository-Zugriff für seine Mitglieder. Fortfahren? teams.delete_team_success=Das Team wurde gelöscht. teams.read_permission_desc=Dieses Team hat Lesezugriff: Mitglieder können Team-Repositories einsehen und klonen. teams.write_permission_desc=Dieses Team hat Schreibzugriff: Mitglieder können Team-Repositories einsehen und darauf pushen. @@ -1277,12 +1279,12 @@ dashboard.delete_repo_archives=Alle Repository-Archive löschen dashboard.delete_repo_archives_success=Alle Repository-Archive wurden gelöscht. dashboard.delete_missing_repos=Alle Repository-Datensätze mit verlorenen gegangenen Git-Dateien löschen dashboard.delete_missing_repos_success=Alle Repository-Datensätze mit verlorenen Git-Dateien wurden gelöscht. -dashboard.git_gc_repos=Garbage Collection auf Repositories ausführen -dashboard.git_gc_repos_success=Alle Repositories haben Garbage Collection beendet. -dashboard.resync_all_sshkeys='.ssh/authorized_keys'-Datei mit Gitea SSH-Keys neu schreiben. (Wenn Du den eingebauten SSH Server nutzt, musst du das nicht ausführen.) +dashboard.git_gc_repos=Garbage-Collection auf Repositories ausführen +dashboard.git_gc_repos_success=Alle Repositories haben Garbage-Collection beendet. +dashboard.resync_all_sshkeys=„.ssh/authorized_keys“-Datei mit Gitea-SSH-Keys neu schreiben. (Wenn Du den eingebauten SSH-Server nutzt, musst du das nicht ausführen.) dashboard.resync_all_sshkeys_success=Alle von Gitea verwalteten öffentlichen Schlüssel wurden neu geschrieben. -dashboard.resync_all_hooks=Synchronisiere pre-receive, update und post-receive Hooks für alle Repositories. -dashboard.resync_all_hooks_success=Alle pre-receive, update und post-receive Repository-Hooks wurden synchronisiert. +dashboard.resync_all_hooks=Synchronisiere „pre-receive“-, „update“- und „post-receive“-Hooks für alle Repositorys erneut. +dashboard.resync_all_hooks_success=Alle „pre-receive“-, „update“- und „post-receive“-Repository-Hooks wurden erneut synchronisiert. dashboard.reinit_missing_repos=Alle Git-Repositories mit Einträgen neu einlesen dashboard.reinit_missing_repos_success=Alle verlorenen Git-Repositories mit existierenden Einträgen wurden erfolgreich aktualisiert. dashboard.sync_external_users=Externe Benutzerdaten synchronisieren @@ -1305,11 +1307,11 @@ dashboard.heap_memory_released=Freigegebener Heap-Memory dashboard.heap_objects=Heap-Objekte dashboard.bootstrap_stack_usage=Bootstrap-Stack-Auslastung dashboard.stack_memory_obtained=Erhaltener Stack-Memory -dashboard.mspan_structures_usage=MSpan-Structures Auslastung -dashboard.mspan_structures_obtained=MSpan-Structures erhalten -dashboard.mcache_structures_usage=MCache-Structures Auslastung +dashboard.mspan_structures_usage=MSpan-Structures-Auslastung +dashboard.mspan_structures_obtained=Erhaltene MSpan-Structures +dashboard.mcache_structures_usage=MCache-Structures-Auslastung dashboard.mcache_structures_obtained=Erhaltene MCache-Structures -dashboard.profiling_bucket_hash_table_obtained=Analysesatz Hashtabellen erhalten +dashboard.profiling_bucket_hash_table_obtained=Erhaltene Analysesatz-Hashtabellen dashboard.gc_metadata_obtained=Erhaltene GC-Metadata dashboard.other_system_allocation_obtained=Andere erhaltene System-Allokationen dashboard.next_gc_recycle=Nächster GC-Zyklus @@ -1342,7 +1344,7 @@ users.max_repo_creation_desc=(Gib -1 ein, um das globale Standardlimit zu verwen users.is_activated=Account ist aktiviert users.prohibit_login=Anmelden deaktivieren users.is_admin=Ist Administrator -users.allow_git_hook=Darf "Git Hooks" erstellen +users.allow_git_hook=Darf „Git Hooks“ erstellen users.allow_import_local=Darf lokale Repositories importieren users.allow_create_organization=Darf Organisationen erstellen users.update_profile=Benutzerkonto aktualisieren @@ -1384,31 +1386,31 @@ auths.bind_dn=DN binden auths.bind_password=Passwort binden auths.bind_password_helper=Achtung: Das Passwort wird im Klartext gespeichert. Benutze wenn möglich einen Account mit nur Lesezugriff. auths.user_base=Basis für Benutzersuche -auths.user_dn=Benutzer DN -auths.attribute_username=Benutzername Attribut +auths.user_dn=Benutzer-DN +auths.attribute_username=Benutzernamens-Attribut auths.attribute_username_placeholder=Leerlassen, um den in Gitea eingegebenen Benutzernamen zu verwenden. auths.attribute_name=Vornamensattribut auths.attribute_surname=Nachnamensattribut -auths.attribute_mail=E-Mail Attribut -auths.attribute_ssh_public_key=Öffentliches SSH-Schlüssel Attribut +auths.attribute_mail=E-Mail-Attribut +auths.attribute_ssh_public_key=Öffentlicher-SSH-Schlüssel-Attribut auths.attributes_in_bind=Hole Attribute im Bind-Kontext auths.use_paged_search=Seitensuche verwenden auths.search_page_size=Seitengröße auths.filter=Benutzerfilter -auths.admin_filter=Admin Filter -auths.ms_ad_sa=MS AD Suchattribute +auths.admin_filter=Admin-Filter +auths.ms_ad_sa=MS-AD-Suchattribute auths.smtp_auth=SMTP-Authentifizierungstyp auths.smtphost=SMTP-Host auths.smtpport=SMTP-Port auths.allowed_domains=Erlaubte Domains -auths.allowed_domains_helper=Leerlassen, um alle Domains zuzulassen. Trenne mehrere Domänen mit einem Komma (','). +auths.allowed_domains_helper=Leerlassen, um alle Domains zuzulassen. Trenne mehrere Domänen mit einem Komma („,“). auths.enable_tls=TLS-Verschlüsselung aktivieren -auths.skip_tls_verify=TLS Verifikation überspringen -auths.pam_service_name=PAM Dienstname -auths.oauth2_provider=OAuth2 Anbieter +auths.skip_tls_verify=TLS-Verifikation überspringen +auths.pam_service_name=PAM-Dienstname +auths.oauth2_provider=OAuth2-Anbieter auths.oauth2_clientID=Client-ID (Schlüssel) auths.oauth2_clientSecret=Client-Secret -auths.openIdConnectAutoDiscoveryURL=OpenID Connect Auto Discovery URL +auths.openIdConnectAutoDiscoveryURL=OpenID-Connect-Auto-Discovery-URL auths.oauth2_use_custom_url=Benutzerdefinierte URLs anstelle von Standard-URLs verwenden auths.oauth2_tokenURL=Token-URL auths.oauth2_authURL=Authorisierungs-URL @@ -1416,48 +1418,48 @@ auths.oauth2_profileURL=Profil-URL auths.oauth2_emailURL=E-Mail-URL auths.enable_auto_register=Automatische Registrierung aktivieren auths.tips=Tipps -auths.tips.oauth2.general=OAuth2 Authentifizierung -auths.tips.oauth2.general.tip=Beim Registrieren einer neuen OAuth2 Authentifizierung sollte die Callback/Weiterleitungs-URL /user/oauth2//callback sein. -auths.tip.oauth2_provider=OAuth2 Anbieter -auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter https://bitbucket.org/account/user//oauth-consumers/new und füge die Berechtigung "Account"-"Read" hinzu. +auths.tips.oauth2.general=OAuth2-Authentifizierung +auths.tips.oauth2.general.tip=Beim Registrieren einer neuen OAuth2-Authentifizierung sollte die Callback-/Weiterleitungs-URL „/user/oauth2//callback“ sein. +auths.tip.oauth2_provider=OAuth2-Anbieter +auths.tip.bitbucket=Registriere einen neuen OAuth-Consumer unter https://bitbucket.org/account/user//oauth-consumers/new und füge die Berechtigung „Account“ – „Read“ hinzu. auths.tip.dropbox=Erstelle eine neue App auf https://www.dropbox.com/developers/apps. -auths.tip.facebook=Erstelle eine neue Anwendung auf https://developers.facebook.com/apps und füge das Produkt "Facebook Login" hinzu. -auths.tip.github=Erstelle unter https://github.com/settings/applications/new eine neue OAuth Anwendung. +auths.tip.facebook=Erstelle eine neue Anwendung auf https://developers.facebook.com/apps und füge das Produkt „Facebook Login“ hinzu. +auths.tip.github=Erstelle unter https://github.com/settings/applications/new eine neue OAuth-Anwendung. auths.tip.gitlab=Erstelle unter https://gitlab.com/profile/applications eine neue Anwendung. -auths.tip.google_plus=Du erhältst die OAuth2 Client Zugangsdaten in der Google API Console unter https://console.developers.google.com/ +auths.tip.google_plus=Du erhältst die OAuth2-Client-Zugangsdaten in der Google-API-Konsole unter https://console.developers.google.com/ auths.tip.openid_connect=Benutze die OpenID Connect Discovery URL (/.well-known/openid-configuration) als Endpunkt. -auths.tip.twitter=Gehe auf https://dev.twitter.com/apps, erstelle eine Anwendung und stelle sicher, dass die Option “Allow this application to be used to Sign in with Twitter” aktiviert ist +auths.tip.twitter=Gehe auf https://dev.twitter.com/apps, erstelle eine Anwendung und stelle sicher, dass die Option „Allow this application to be used to Sign in with Twitter“ aktiviert ist auths.edit=Authentifikationsquelle bearbeiten auths.activated=Diese Authentifikationsquelle ist aktiviert -auths.new_success=Die Authentifizierung "%s" wurde hinzugefügt. +auths.new_success=Die Authentifizierung „%s“ wurde hinzugefügt. auths.update_success=Diese Authentifizierungsquelle wurde aktualisiert. auths.update=Authentifizierungsquelle aktualisieren auths.delete=Authentifikationsquelle löschen auths.delete_auth_title=Authentifizierungsquelle löschen auths.delete_auth_desc=Das Löschen einer Authentifizierungsquelle verhindert, dass Benutzer sich darüber anmelden können. Fortfahren? auths.still_in_used=Diese Authentifizierungsquelle wird noch verwendet. Bearbeite oder lösche zuerst alle Benutzer, die diese Authentifizierungsquelle benutzen. -auths.deletion_success=Die Authentifizierungsquelle '%s' wurde gelöscht. -auths.login_source_exist=Die Authentifizierungsquelle '%s' existiert bereits. +auths.deletion_success=Die Authentifizierungsquelle „%s“ wurde gelöscht. +auths.login_source_exist=Die Authentifizierungsquelle „%s“ existiert bereits. config.server_config=Serverkonfiguration config.app_name=Seitentitel -config.app_ver=Gitea Version -config.app_url=Gitea Basis-URL +config.app_ver=Gitea-Version +config.app_url=Gitea-Basis-URL config.custom_conf=Konfigurations-Datei-Pfad -config.domain=SSH Server-Domain +config.domain=SSH-Server-Domain config.offline_mode=Lokaler Modus config.disable_router_log=Router-Log deaktivieren config.run_user=Ausführen als config.run_mode=Laufzeit-Modus -config.git_version=Git Version -config.repo_root_path=Repository-Verzeichnis +config.git_version=Git-Version +config.repo_root_path=Repository-Wurzelpfad config.lfs_root_path=LFS-Wurzelpfad config.static_file_root_path=Verzeichnis für statische Dateien config.log_file_root_path=Logdateipfad config.script_type=Skript-Typ config.reverse_auth_user=Nutzer bei Reverse-Authentifizierung -config.ssh_config=SSH Konfiguration +config.ssh_config=SSH-Konfiguration config.ssh_enabled=Aktiviert config.ssh_start_builtin_server=Eingebauten Server verwenden config.ssh_domain=Server-Domain @@ -1465,9 +1467,9 @@ config.ssh_port=Port config.ssh_listen_port=Listen-Port config.ssh_root_path=Wurzelverzeichnis config.ssh_key_test_path=Schlüssel-Test-Pfad -config.ssh_keygen_path=Keygen ('ssh-keygen') Pfad +config.ssh_keygen_path=Keygen-Pfad („ssh-keygen“) config.ssh_minimum_key_size_check=Prüfung der Mindestschlüssellänge -config.ssh_minimum_key_sizes=Minimale Schlüssellängen +config.ssh_minimum_key_sizes=Mindestschlüssellängen config.db_config=Datenbankkonfiguration config.db_type=Typ @@ -1481,17 +1483,17 @@ config.service_config=Service-Konfiguration config.register_email_confirm=E-Mail-Bestätigung benötigt zum Registrieren config.disable_register=Selbstegistrierung deaktivieren config.allow_only_external_registration=Registrierung nur über externe Services aktiveren -config.enable_openid_signup=OpenID Selbstregistrierung aktivieren -config.enable_openid_signin=OpenID Anmeldung aktivieren +config.enable_openid_signup=OpenID-Selbstregistrierung aktivieren +config.enable_openid_signin=OpenID-Anmeldung aktivieren config.show_registration_button=Schaltfläche zum Registrieren anzeigen config.require_sign_in_view=Seiten nur für angemeldete Benutzer zugänglich config.mail_notify=E-Mail-Benachrichtigungen aktivieren config.disable_key_size_check=Prüfung der Mindestschlüssellänge deaktiveren config.enable_captcha=CAPTCHA aktivieren -config.active_code_lives=Aktivierungscode Lebensdauer +config.active_code_lives=Aktivierungscode-Lebensdauer config.reset_password_code_lives=Ablaufdatum des Passworts zurücksetzen config.default_keep_email_private=E-Mail-Adressen standardmäßig verbergen -config.default_allow_create_organization=Erstellen von Organisationen standarmäßig erlauben +config.default_allow_create_organization=Erstellen von Organisationen standardmäßig erlauben config.enable_timetracking=Zeiterfassung aktivieren config.default_enable_timetracking=Zeiterfassung standardmäßig aktivieren config.default_allow_only_contributors_to_track_time=Nur Mitarbeitern erlauben, die Zeiterfassung zu nutzen @@ -1500,11 +1502,11 @@ config.no_reply_address=Versteckte E-Mail-Domain config.webhook_config=Webhook-Konfiguration config.queue_length=Warteschlangenlänge config.deliver_timeout=Zeitlimit für Zustellung -config.skip_tls_verify=TLS Verifikation überspringen +config.skip_tls_verify=TLS-Verifikation überspringen -config.mailer_config=SMTP Mailer Konfiguration +config.mailer_config=SMTP-Mailer-Konfiguration config.mailer_enabled=Aktiviert -config.mailer_disable_helo=HELO Deaktivieren +config.mailer_disable_helo=HELO deaktivieren config.mailer_name=Name config.mailer_host=Host config.mailer_user=Benutzer @@ -1512,8 +1514,8 @@ config.mailer_use_sendmail=Sendmail benutzen config.mailer_sendmail_path=Sendmail-Pfad config.mailer_sendmail_args=Zusätzliche Argumente für Sendmail config.send_test_mail=Test-E-Mail senden -config.test_mail_failed=Das Senden der Test-E-Mail an '%s' ist fehlgeschlagen: %v -config.test_mail_sent=Eine Test-E-Mail wurde an '%s' gesendet. +config.test_mail_failed=Das Senden der Test-E-Mail an „%s“ ist fehlgeschlagen: %v +config.test_mail_sent=Eine Test-E-Mail wurde an „%s“ gesendet. config.oauth_config=OAuth-Konfiguration config.oauth_enabled=Aktiviert @@ -1533,16 +1535,16 @@ config.session_life_time=Session-Lebensdauer config.https_only=Nur HTTPS config.cookie_life_time=Cookie-Lebensdauer -config.picture_config=Avatar-Konfiguration +config.picture_config=Bild-und-Profilbild-Konfiguration config.picture_service=Bilderservice config.disable_gravatar=Gravatar deaktivieren config.enable_federated_avatar=Föderierte Profilbilder einschalten -config.git_config=Git Konfiguration -config.git_disable_diff_highlight=Diff Syntaxhervorhebung ausschalten -config.git_max_diff_lines=Max Diff Zeilen (in einer Datei) -config.git_max_diff_line_characters=Max Diff Zeichen (in einer Zeile) -config.git_max_diff_files=Max Diff Dateien (Anzeige) +config.git_config=Git-Konfiguration +config.git_disable_diff_highlight=Diff-Syntaxhervorhebung ausschalten +config.git_max_diff_lines=Max. Diff-Zeilen (in einer Datei) +config.git_max_diff_line_characters=Max. Diff-Zeichen (in einer Zeile) +config.git_max_diff_files=Max. Diff-Dateien (Angezeigte) config.git_gc_args=GC-Argumente config.git_migrate_timeout=Zeitlimit für Migration config.git_mirror_timeout=Zeitlimit für Mirror-Aktualisierung @@ -1638,12 +1640,12 @@ mark_all_as_read=Alle als gelesen markieren [gpg] error.extract_sign=Die Signatur konnte nicht extrahiert werden error.generate_hash=Es konnte kein Hash vom Commit generiert werden -error.no_committer_account=Es ist kein Benutzerkonto mit dieser Commiter-Email verbunden +error.no_committer_account=Es ist kein Benutzerkonto mit der E-Mail-Adresse des Committers verbunden error.no_gpg_keys_found=Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden error.not_signed_commit=Kein signierter Commit error.failed_retrieval_gpg_keys=Fehler beim Abrufen eines Keys des Commiter-Kontos [units] -error.no_unit_allowed_repo=Du hast keine Berechtigung auf einen Bereich dieses Repositories zuzugreifen. -error.unit_not_allowed=Du hast keine Berechtigung auf diesen Repository-Bereich zuzugreifen. +error.no_unit_allowed_repo=Du hast keine Berechtigung, um auf irgendeinen Bereich dieses Repositories zuzugreifen. +error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bereich zuzugreifen. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 8cf6111c6..21ae775e4 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1167,6 +1167,8 @@ branch.protected_deletion_failed = Branch '%s' is protected. It cannot be delete topic.manage_topics = Manage Topics topic.done = Done +topic.count_prompt = You can't select more than 25 topics +topic.format_prompt = Topics must start with a letter or number, can include hyphens(-) and must be no more than 35 characters long [org] org_name_holder = Organization Name diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index f49b98280..c6855090a 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -40,6 +40,7 @@ u2f_unsupported_browser=Ваш браузер не підтримує U2F клю u2f_error_1=Сталася невідома помилка. Спробуйте ще раз. u2f_error_2=Переконайтеся, що ви використовуєте зашифроване з'єднання (https://) та відвідуєте правильну URL-адресу. u2f_error_3=Сервер не може обробити, ваш запит. +u2f_error_5=Таймаут досягнуто до того, як ваш ключ можна буде прочитати. Перезавантажте, щоб повторити спробу. u2f_reload=Оновити repository=Репозиторій @@ -105,6 +106,7 @@ ssh_port_helper=Номер порту, який використовує SSH с http_port=Gitea HTTP порт http_port_helper=Номер порту, який буде прослуховуватися Giteas веб-сервером. app_url=Базова URL-адреса Gitea +app_url_helper=Базова адреса для HTTP(S) клонування через URL та повідомлень електронної пошти. log_root_path=Шлях до лог файлу log_root_path_helper=Файли журналу будуть записані в цей каталог. @@ -112,6 +114,7 @@ optional_title=Додаткові налаштування email_title=Налаштування Email smtp_host=SMTP хост smtp_from=Відправляти Email від імені +smtp_from_helper=Електронна пошта для використання в Gіtea. Введіть звичайну електронну адресу або використовуйте формат: "Ім'я" . mailer_user=SMTP Ім'я кристувача mailer_password=SMTP Пароль register_confirm=Потрібно підтвердити електронну пошту для реєстрації @@ -125,9 +128,11 @@ federated_avatar_lookup=Увімкнути федеративні аватари federated_avatar_lookup_popup=Увімкнути зовнішний Аватар за допомогою Libravatar. disable_registration=Вимкнути самостійну реєстрацію disable_registration_popup=Вимкнути самостійну реєстрацію користувачів, тільки адміністратор може створювати нові облікові записи. +allow_only_external_registration_popup=Включити реєстрацію тільки через зовнішні сервіси. openid_signin=Увімкнути реєстрацію за допомогою OpenID openid_signin_popup=Увімкнути вхід за допомогою OpenID. openid_signup=Увімкнути самостійну реєстрацію за допомогою OpenID +openid_signup_popup=Увімкнути самореєстрацію користувачів на основі OpenID. enable_captcha=Увімкнути CAPTCHA enable_captcha_popup=Вимагати перевірку CAPTCHA при самостійній реєстрації користувача. require_sign_in_view=Вимагати авторизації для перегляду сторінок @@ -141,6 +146,7 @@ admin_email=Адреса електронної пошти install_btn_confirm=Встановлення Gitea test_git_failed=Не в змозі перевірити 'git' команду: %v invalid_db_setting=Налаштування бази даних є некоректними: %v +invalid_repo_path=Помилковий шлях до кореня репозиторію: %v save_config_failed=Не в змозі зберегти конфігурацію: %v invalid_admin_setting=Неприпустимі налаштування облікового запису адміністратора: %v install_success=Ласкаво просимо! Дякуємо вам за вибір Gitea. Розважайтеся, і будьте обережні! @@ -152,6 +158,7 @@ default_allow_create_organization_popup=Дозволити новим облік default_enable_timetracking=Увімкнути відстеження часу за замовчуванням default_enable_timetracking_popup=Включити відстеження часу для нових репозиторіїв за замовчуванням. no_reply_address=Прихований поштовий домен +no_reply_address_helper=Доменне ім'я для користувачів із прихованою електронною адресою. Наприклад, ім'я користувача 'joe' буде входити в Git як 'joe@noreply.example.org', якщо для прихованого домену електронної пошти встановлено 'noreply.example.org'. [home] uname_holder=Ім'я користувача або Ел. пошта @@ -176,11 +183,13 @@ code=Код repo_no_results=Відповідних репозиторіїв не знайдено. user_no_results=Відповідних користувачів не знайдено. org_no_results=Відповідних організацій не знайдено. +code_no_results=Відповідний пошуковому запитанню код не знайдено. code_search_results=Результати пошуку '%s' [auth] create_new_account=Реєстрація облікового запису register_helper_msg=Вже зареєстровані? Увійдіть зараз! +social_register_helper_msg=Вже є аккаунт? Зв'яжіть його зараз! disable_register_prompt=Вибачте, можливість реєстрації відключена. Будь ласка, зв'яжіться з адміністратором сайту. disable_register_mail=Підтвердження реєстрації електронною поштою вимкнено. remember_me=Запам'ятати мене @@ -200,18 +209,22 @@ send_reset_mail=Натисніть сюди, щоб відправити лис reset_password=Скинути пароль invalid_code=Цей код підтвердження недійсний або закінчився. reset_password_helper=Натисніть тут для скидання пароля +password_too_short=Довжина пароля не може бути меншою за %d символів. non_local_account=Нелокальні акаунти не можуть змінити пароль через Gitea. verify=Підтвердити scratch_code=Одноразовий пароль use_scratch_code=Використовувати одноразовий пароль -twofa_scratch_used=Ви використовували одноразовий пароль. Ви були перенаправлені на сторінку налаштувань для генерації нового коду або відключення двуфакторной аутентифікації. +twofa_scratch_used=Ви використовували одноразовий пароль. Ви були перенаправлені на сторінку налаштувань для генерації нового коду або відключення двуфакторної автентифікації. twofa_passcode_incorrect=Ваш пароль є невірним. Якщо ви втратили пристрій, використовуйте ваш одноразовий пароль. twofa_scratch_token_incorrect=Невірний одноразовий пароль. login_userpass=Увійти login_openid=OpenID openid_connect_submit=Під’єднатися openid_connect_title=Підключитися до існуючого облікового запису +openid_connect_desc=Вибраний OpenID URI невідомий. Пов'яжіть його з новим обліковим записом тут. openid_register_title=Створити новий обліковий запис +openid_register_desc=Вибраний OpenID URI невідомий. Пов'яжіть йогоз новим обліковим записом тут. +openid_signin_desc=Введіть свій ідентифікатор OpenID. Наприклад: https://anne.me, bob.openid.org.cn або gnusocial.net/carry. disable_forgot_password_mail=На жаль скидання пароля відключене. Будь ласка, зв'яжіться з адміністратором сайту. [mail] @@ -247,6 +260,8 @@ TreeName=Шлях до файлу Content=Зміст require_error=` не може бути пустим.` +alpha_dash_error=` повинен містити тільки літерно-цифрові символи, дефіс ('-') та підкреслення ('_'). ` +alpha_dash_dot_error=` повинен містити тільки літерно-цифрові символи, дефіс ('-') , підкреслення ('_') та точки ('.'). ` git_ref_name_error=` повинен бути правильним посилальним ім'ям Git.` size_error=` повинен бути розмір %s.` min_size_error=` повинен бути принаймні %s символів.` @@ -262,6 +277,7 @@ username_been_taken=Ім'я користувача вже зайнято. repo_name_been_taken=Ім'я репозіторію вже використовується. org_name_been_taken=Назва організації вже зайнято. team_name_been_taken=Назва команди вже зайнято. +team_no_units_error=Дозволити доступ до принаймні одного розділу репозитарію. email_been_used=Ця електронна адреса вже використовується. openid_been_used=OpenID адреса '%s' вже використовується. username_password_incorrect=Неправильне ім'я користувача або пароль. @@ -269,27 +285,31 @@ enterred_invalid_repo_name=Невірно введено ім'я репозит enterred_invalid_owner_name=Ім'я нового власника не є дійсним. enterred_invalid_password=Введений вами пароль некоректний. user_not_exist=Даний користувач не існує. +last_org_owner=Ви не можете вилучити останнього користувача з команди 'власники'. У кожній команді має бути хоча б один власник. cannot_add_org_to_team=Організацію неможливо додати як учасника команди. invalid_ssh_key=Неможливо перевірити ваш SSH ключ: %s invalid_gpg_key=Неможливо перевірити ваш GPG ключ: %s +unable_verify_ssh_key=Не вдається підтвердити ключ SSH; подвійно перевірте його на наявність похибки. auth_failed=Помилка автентифікації: %v +still_own_repo=Ваш обліковий запис володіє одним або декількома репозиторіями; видаліть або перенесіть їх в першу чергу. target_branch_not_exist=Цільової гілки не існує. [user] change_avatar=Змінити свій аватар… -join_on=Приєднався +join_on=Приєднався(-лась) repositories=Репозиторії activity=Публічна активність -followers=Підписники +followers=Читачі starred=Обрані Репозиторії -following=Слідкувати +following=Читає follow=Підписатися unfollow=Відписатися form.name_reserved=Ім'я користувача "%s" зарезервовано. +form.name_pattern_not_allowed=Шаблон '%s' не дозволено в імені користувача. [settings] profile=Профіль @@ -311,6 +331,7 @@ u2f=Ключі безпеки public_profile=Загальнодоступний профіль profile_desc=Ваша адреса електронної пошти використовуватиметься для сповіщення та інших операцій. +password_username_disabled=Нелокальним користувачам заборонено змінювати ім'я користувача. Щоб отримати докладнішу інформацію, зв'яжіться з адміністратором сайту. full_name=Повне ім'я website=Веб-сайт location=Місцезнаходження @@ -340,28 +361,36 @@ password_change_disabled=Нелокальні акаунти не можуть emails=Адреса електронної пошти manage_emails=Керування адресами ел. пошти +manage_openid=Керування OpenID email_desc=Ваша основна адреса електронної пошти використовуватиметься для сповіщення та інших операцій. primary=Основний primary_email=Зробити основним delete_email=Видалити email_deletion=Видалити адресу електронної пошти openid_deletion=Видалити адресу OpenID +openid_deletion_success=Адреса OpenID була видалена. add_new_email=Додати нову адресу електронної пошти add_new_openid=Додати новий OpenID URI add_email=Додати адресу електронної пошти add_openid=Додати OpenID URI add_email_confirmation_sent=Електронний лист із підтвердженням було відправлено на '%s', будь ласка, перевірте вашу поштову скриньку протягом наступних %s, щоб підтвердити адресу. add_email_success=Додано нову адресу електронної пошти. +add_openid_success=Нова адреса OpenID була додана. keep_email_private=Приховати адресу електронної пошти keep_email_private_popup=Вашу адресу електронної пошти буде приховано від інших користувачів. manage_ssh_keys=Керувати SSH ключами manage_gpg_keys=Керувати GPG ключами add_key=Додати ключ +ssh_desc=Ці відкриті SSH-ключі пов'язані з вашим обліковим записом. Відповідні приватні ключі дозволяють отримати повний доступ до ваших репозиторіїв. +gpg_desc=Ці публічні ключі GPG пов'язані з вашим обліковим записом. Тримайте свої приватні ключі в безпеці, оскільки вони дозволяють здійснювати перевірку комітів. ssh_helper=Потрібна допомога? Дивіться гід на GitHub з генерації ключів SSH або виправлення типових неполадок SSH. gpg_helper= Потрібна допомога? Перегляньте посібник GitHub про GPG . add_new_key=Додати SSH ключ add_new_gpg_key=Додати GPG ключ +ssh_key_been_used=Цей ключ SSH вже додано до вашого облікового запису. +ssh_key_name_used=Ключ SSH з таким самим ім'ям вже додано до вашого облікового запису. +gpg_key_id_used=Публічний ключ GPG з таким самим ідентифікатором вже існує. subkeys=Підключі key_id=ID ключа key_name=Ім'я ключа @@ -399,20 +428,25 @@ generate_token=Згенерувати токен delete_token=Видалити access_token_deletion=Видалити токен доступу -twofa_desc=Двофакторна аутентифікація підвищує безпеку вашого облікового запису. +twofa_desc=Двофакторна автентифікація підвищує безпеку вашого облікового запису. twofa_is_enrolled=Ваш обліковий запис на даний час використовує двофакторну автентифікацію. twofa_disable=Вимкнути двофакторну автентифікацію +twofa_scratch_token_regenerate=Перестворити токен одноразового пароля twofa_enroll=Увімкнути двофакторну автентифікацію +twofa_disable_note=При необхідності можна відключити двофакторну автентифікацію. +regenerate_scratch_token_desc=Якщо ви втратили свій токен одноразового пароля або вже використовували його для входу, ви можете скинути його тут. twofa_disabled=Двофакторна автентифікація вимкнена. -scan_this_image=Проскануйте це зображення вашим додатком для двуфакторної аутентифікації: +scan_this_image=Проскануйте це зображення вашим додатком для двуфакторної автентифікації: or_enter_secret=Або введіть секрет: %s passcode_invalid=Некоректний пароль. Спробуй ще раз. +u2f_desc=Ключами безпеки є апаратні пристрої, що містять криптографічні ключі. Вони можуть використовуватися для двофакторної автентифікації. Ключ безпеки повинен підтримувати стандарт FIDO U2F. u2f_register_key=Додати ключ безпеки u2f_nickname=Псевдонім u2f_delete_key=Видалити ключ безпеки manage_account_links=Керування обліковими записами +manage_account_links_desc=Ці зовнішні акаунти прив'язані до вашого аккаунту Gitea. remove_account_link=Видалити облікові записи orgs_none=Ви не є учасником будь-якої організації. @@ -428,6 +462,7 @@ owner=Власник repo_name=Назва репозиторію visibility=Видимість visiblity_helper=Зробити репозиторій приватним +visiblity_fork_helper=(Зміна цього вплине на всі форки.) clone_helper=Потрібна допомога у клонуванні? Відвідайте Допомогу. fork_repo=Форкнути репозиторій fork_from=Форк з @@ -439,6 +474,7 @@ license=Ліцензія license_helper=Виберіть ліцензійний файл. readme=README readme_helper=Виберіть шаблон README. +auto_init=Ініціалізувати репозиторій (Додає .gitignore, LICENSE та README) create_repo=Створити репозиторій default_branch=Головна гілка mirror_prune=Очистити @@ -553,6 +589,7 @@ commits.date=Дата commits.older=Давніше commits.newer=Новіше commits.signed_by=Підписано +commits.gpg_key_id=Ідентифікатор GPG ключа ext_issues=Зов. Проблеми @@ -603,7 +640,10 @@ issues.filter_sort.recentupdate=Нещодавно оновлено issues.filter_sort.leastupdate=Найдавніше оновлені issues.filter_sort.mostcomment=Найбільш коментовані issues.filter_sort.leastcomment=Найменш коментовані +issues.filter_sort.moststars=Найбільш обраних +issues.filter_sort.feweststars=Найменш обраних issues.filter_sort.mostforks=Найбільше форків +issues.filter_sort.fewestforks=Найменше форків issues.action_open=Відкрити issues.action_close=Закрити issues.action_label=Мітка @@ -721,6 +761,8 @@ milestones.edit=Редагувати етап milestones.cancel=Відмінити milestones.modify=Оновити етап milestones.deletion=Видалити етап +milestones.filter_sort.closest_due_date=Найближче за датою +milestones.filter_sort.furthest_due_date=Далі за датою milestones.filter_sort.most_issues=Найбільш проблем milestones.filter_sort.least_issues=Найменш проблем @@ -968,6 +1010,7 @@ topic.done=Готово [org] org_name_holder=Назва організації org_full_name_holder=Повна назва організації +org_name_helper=Назва організації має бути простою та зрозумілою. create_org=Створити організацію repo_updated=Оновлено people=Учасники @@ -1096,7 +1139,9 @@ users.admin=Адміністратор users.repos=Репозиторії users.created=Створено users.last_login=Останній вхід +users.never_login=Ніколи не входив users.send_register_notify=Надіслати повідомлення про реєстрацію користувача +users.new_success=Обліковий запис '%s' створений. users.edit=Редагувати users.auth_source=Джерело автентифікації users.local=Локальні @@ -1130,7 +1175,7 @@ repos.forks=Форки repos.issues=Проблеми repos.size=Розмір -auths.auth_manage_panel=Керування джерелом аутентифікації +auths.auth_manage_panel=Керування джерелом автентифікації auths.new=Додати джерело автентифікації auths.name=Ім'я auths.type=Тип @@ -1167,8 +1212,8 @@ auths.oauth2_profileURL=URL профілю auths.oauth2_emailURL=URL електронної пошти auths.enable_auto_register=Увімкнути автоматичну реєстрацію auths.tips=Поради -auths.tips.oauth2.general=OAuth2 аутентифікація -auths.tips.oauth2.general.tip=При додаванні нового OAuth2 провайдера, URL адреса переадресації по завершенні аутентифікації повинена виглядати так:/user/oauth2//callback +auths.tips.oauth2.general=OAuth2 автентифікація +auths.tips.oauth2.general.tip=При додаванні нового OAuth2 провайдера, URL адреса переадресації по завершенні автентифікації повинена виглядати так:/user/oauth2//callback auths.tip.oauth2_provider=Постачальник OAuth2 auths.tip.dropbox=Додайте новий додаток на https://www.dropbox.com/developers/apps auths.tip.facebook=Створіть новий додаток на https://developers.facebook.com/apps і додайте модуль "Facebook Login @@ -1232,6 +1277,7 @@ config.default_keep_email_private=Приховати адресу електро config.default_allow_create_organization=Дозволити створення організацій за замовчуванням config.enable_timetracking=Увімкнути відстеження часу config.default_enable_timetracking=Увімкнути відстеження часу за замовчуванням +config.no_reply_address=Прихований домен електронної пошти config.webhook_config=Конфігурація web-хуків config.queue_length=Довжина черги diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 658f133a2..4e59ed08e 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -213,6 +213,7 @@ send_reset_mail=单击此处(重新)发送您的密码重置邮件 reset_password=重置密码 invalid_code=此确认密钥无效或已过期。 reset_password_helper=单击此处重置密码 +password_too_short=密码长度不能少于 %d 位。 non_local_account=非本地帐户不能通过 Gitea 的 web 界面更改密码。 verify=验证 scratch_code=验证口令 diff --git a/public/js/index.js b/public/js/index.js index e98a3fe6d..823dd8766 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -2336,8 +2336,10 @@ function initTopicbar() { }).done(function() { editDiv.hide(); viewDiv.show(); + }).fail(function(xhr) { + alert(xhr.responseJSON.message) }) - }) + }); $('#topic_edit .dropdown').dropdown({ allowAdditions: true, diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 211d8045a..7be39166d 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -1,4 +1,5 @@ // Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2018 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. @@ -165,7 +166,7 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { // "$ref": "#/responses/Issue" var deadlineUnix util.TimeStamp - if form.Deadline != nil { + if form.Deadline != nil && ctx.Repo.IsWriter() { deadlineUnix = util.TimeStamp(form.Deadline.Unix()) } @@ -178,15 +179,22 @@ func CreateIssue(ctx *context.APIContext, form api.CreateIssueOption) { DeadlineUnix: deadlineUnix, } - // Get all assignee IDs - assigneeIDs, err := models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) - if err != nil { - if models.IsErrUserNotExist(err) { - ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) - } else { - ctx.Error(500, "AddAssigneeByName", err) + var assigneeIDs = make([]int64, 0) + var err error + if ctx.Repo.IsWriter() { + issue.MilestoneID = form.Milestone + assigneeIDs, err = models.MakeIDsFromAPIAssigneesToAdd(form.Assignee, form.Assignees) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.Error(422, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err)) + } else { + ctx.Error(500, "AddAssigneeByName", err) + } + return } - return + } else { + // setting labels is not allowed if user is not a writer + form.Labels = make([]int64, 0) } if err := models.NewIssue(ctx.Repo.Repository, issue, form.Labels, assigneeIDs, nil); err != nil { diff --git a/routers/org/teams.go b/routers/org/teams.go index d894c8661..87bfb8596 100644 --- a/routers/org/teams.go +++ b/routers/org/teams.go @@ -182,7 +182,14 @@ func NewTeamPost(ctx *context.Context, form auth.CreateTeamForm) { Authorize: models.ParseAccessMode(form.Permission), } if t.Authorize < models.AccessModeAdmin { - t.UnitTypes = form.Units + var units = make([]*models.TeamUnit, 0, len(form.Units)) + for _, tp := range form.Units { + units = append(units, &models.TeamUnit{ + OrgID: ctx.Org.Organization.ID, + Type: tp, + }) + } + t.Units = units } ctx.Data["Team"] = t @@ -264,9 +271,17 @@ func EditTeamPost(ctx *context.Context, form auth.CreateTeamForm) { } t.Description = form.Description if t.Authorize < models.AccessModeAdmin { - t.UnitTypes = form.Units + var units = make([]models.TeamUnit, 0, len(form.Units)) + for _, tp := range form.Units { + units = append(units, models.TeamUnit{ + OrgID: t.OrgID, + TeamID: t.ID, + Type: tp, + }) + } + models.UpdateTeamUnits(t, units) } else { - t.UnitTypes = nil + models.UpdateTeamUnits(t, nil) } if ctx.HasError() { diff --git a/routers/repo/topic.go b/routers/repo/topic.go index 2a43d53ff..63fcf793f 100644 --- a/routers/repo/topic.go +++ b/routers/repo/topic.go @@ -12,8 +12,8 @@ import ( "code.gitea.io/gitea/modules/log" ) -// TopicPost response for creating repository -func TopicPost(ctx *context.Context) { +// TopicsPost response for creating repository +func TopicsPost(ctx *context.Context) { if ctx.User == nil { ctx.JSON(403, map[string]interface{}{ "message": "Only owners could change the topics.", @@ -27,6 +27,37 @@ func TopicPost(ctx *context.Context) { topics = strings.Split(topicsStr, ",") } + invalidTopics := make([]string, 0) + i := 0 + for _, topic := range topics { + topic = strings.TrimSpace(strings.ToLower(topic)) + // ignore empty string + if len(topic) > 0 { + topics[i] = topic + i++ + } + if !models.ValidateTopic(topic) { + invalidTopics = append(invalidTopics, topic) + } + } + topics = topics[:i] + + if len(topics) > 25 { + ctx.JSON(422, map[string]interface{}{ + "invalidTopics": topics[:0], + "message": ctx.Tr("repo.topic.count_prompt"), + }) + return + } + + if len(invalidTopics) > 0 { + ctx.JSON(422, map[string]interface{}{ + "invalidTopics": invalidTopics, + "message": ctx.Tr("repo.topic.format_prompt"), + }) + return + } + err := models.SaveTopics(ctx.Repo.Repository.ID, topics...) if err != nil { log.Error(2, "SaveTopics failed: %v", err) diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index 63450ed88..53c1afe66 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -210,7 +210,7 @@ func GogsHooksNewPost(ctx *context.Context, form auth.NewGogshookForm) { Secret: form.Secret, HookEvent: ParseHookEvent(form.WebhookForm), IsActive: form.Active, - HookTaskType: models.GITEA, + HookTaskType: models.GOGS, OrgID: orCtx.OrgID, } if err := w.UpdateEvent(); err != nil { diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 15b91f159..1eefbf1b6 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -486,7 +486,7 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("/:id", repo.WebHooksEdit) m.Post("/:id/test", repo.TestWebhook) m.Post("/gitea/:id", bindIgnErr(auth.NewWebhookForm{}), repo.WebHooksEditPost) - m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksNewPost) + m.Post("/gogs/:id", bindIgnErr(auth.NewGogshookForm{}), repo.GogsHooksEditPost) m.Post("/slack/:id", bindIgnErr(auth.NewSlackHookForm{}), repo.SlackHooksEditPost) m.Post("/discord/:id", bindIgnErr(auth.NewDiscordHookForm{}), repo.DiscordHooksEditPost) m.Post("/dingtalk/:id", bindIgnErr(auth.NewDingtalkHookForm{}), repo.DingtalkHooksEditPost) @@ -626,7 +626,7 @@ func RegisterRoutes(m *macaron.Macaron) { }, context.RepoAssignment(), context.UnitTypes(), context.LoadRepoUnits(), context.CheckUnit(models.UnitTypeReleases)) m.Group("/:username/:reponame", func() { - m.Post("/topics", repo.TopicPost) + m.Post("/topics", repo.TopicsPost) }, context.RepoAssignment(), reqRepoAdmin) m.Group("/:username/:reponame", func() { diff --git a/routers/user/home.go b/routers/user/home.go index 2a193bbde..0c84b2498 100644 --- a/routers/user/home.go +++ b/routers/user/home.go @@ -203,7 +203,11 @@ func Issues(ctx *context.Context) { return } } else { - userRepoIDs, err = ctxUser.GetAccessRepoIDs() + unitType := models.UnitTypeIssues + if isPullList { + unitType = models.UnitTypePullRequests + } + userRepoIDs, err = ctxUser.GetAccessRepoIDs(unitType) if err != nil { ctx.ServerError("ctxUser.GetAccessRepoIDs", err) return diff --git a/routers/user/home_test.go b/routers/user/home_test.go index a9b146b76..8a3d9b9f5 100644 --- a/routers/user/home_test.go +++ b/routers/user/home_test.go @@ -26,8 +26,8 @@ func TestIssues(t *testing.T) { Issues(ctx) assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - assert.EqualValues(t, map[int64]int64{1: 1, 2: 1}, ctx.Data["Counts"]) + assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.Len(t, ctx.Data["Issues"], 1) - assert.Len(t, ctx.Data["Repos"], 2) + assert.Len(t, ctx.Data["Repos"], 1) } diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go index 966d96aed..bcf602c5e 100644 --- a/routers/user/setting/account.go +++ b/routers/user/setting/account.go @@ -24,12 +24,7 @@ func Account(ctx *context.Context) { ctx.Data["PageIsSettingsAccount"] = true ctx.Data["Email"] = ctx.User.Email - emails, err := models.GetEmailAddresses(ctx.User.ID) - if err != nil { - ctx.ServerError("GetEmailAddresses", err) - return - } - ctx.Data["Emails"] = emails + loadAccountData(ctx) ctx.HTML(200, tplSettingsAccount) } @@ -40,6 +35,8 @@ func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { ctx.Data["PageIsSettingsAccount"] = true if ctx.HasError() { + loadAccountData(ctx) + ctx.HTML(200, tplSettingsAccount) return } @@ -85,15 +82,9 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) { return } - // Add Email address. - emails, err := models.GetEmailAddresses(ctx.User.ID) - if err != nil { - ctx.ServerError("GetEmailAddresses", err) - return - } - ctx.Data["Emails"] = emails - if ctx.HasError() { + loadAccountData(ctx) + ctx.HTML(200, tplSettingsAccount) return } @@ -105,6 +96,8 @@ func EmailPost(ctx *context.Context, form auth.AddEmailForm) { } if err := models.AddEmailAddress(email); err != nil { if models.IsErrEmailAlreadyUsed(err) { + loadAccountData(ctx) + ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) return } @@ -149,6 +142,8 @@ func DeleteAccount(ctx *context.Context) { if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { if models.IsErrUserNotExist(err) { + loadAccountData(ctx) + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) } else { ctx.ServerError("UserSignIn", err) @@ -172,3 +167,12 @@ func DeleteAccount(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/") } } + +func loadAccountData(ctx *context.Context) { + emails, err := models.GetEmailAddresses(ctx.User.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + ctx.Data["Emails"] = emails +} diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go index f292b65d7..ac7252469 100644 --- a/routers/user/setting/applications.go +++ b/routers/user/setting/applications.go @@ -22,12 +22,7 @@ func Applications(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsApplications"] = true - tokens, err := models.ListAccessTokens(ctx.User.ID) - if err != nil { - ctx.ServerError("ListAccessTokens", err) - return - } - ctx.Data["Tokens"] = tokens + loadApplicationsData(ctx) ctx.HTML(200, tplSettingsApplications) } @@ -38,12 +33,8 @@ func ApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) { ctx.Data["PageIsSettingsApplications"] = true if ctx.HasError() { - tokens, err := models.ListAccessTokens(ctx.User.ID) - if err != nil { - ctx.ServerError("ListAccessTokens", err) - return - } - ctx.Data["Tokens"] = tokens + loadApplicationsData(ctx) + ctx.HTML(200, tplSettingsApplications) return } @@ -75,3 +66,12 @@ func DeleteApplication(ctx *context.Context) { "redirect": setting.AppSubURL + "/user/settings/applications", }) } + +func loadApplicationsData(ctx *context.Context) { + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens +} diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go index ef986ef8c..c62b117a7 100644 --- a/routers/user/setting/keys.go +++ b/routers/user/setting/keys.go @@ -23,19 +23,7 @@ func Keys(ctx *context.Context) { ctx.Data["PageIsSettingsKeys"] = true ctx.Data["DisableSSH"] = setting.SSH.Disabled - keys, err := models.ListPublicKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListPublicKeys", err) - return - } - ctx.Data["Keys"] = keys - - gpgkeys, err := models.ListGPGKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListGPGKeys", err) - return - } - ctx.Data["GPGKeys"] = gpgkeys + loadKeysData(ctx) ctx.HTML(200, tplSettingsKeys) } @@ -45,21 +33,9 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsKeys"] = true - keys, err := models.ListPublicKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListPublicKeys", err) - return - } - ctx.Data["Keys"] = keys - - gpgkeys, err := models.ListGPGKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListGPGKeys", err) - return - } - ctx.Data["GPGKeys"] = gpgkeys - if ctx.HasError() { + loadKeysData(ctx) + ctx.HTML(200, tplSettingsKeys) return } @@ -73,9 +49,13 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) { ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error())) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case models.IsErrGPGKeyIDAlreadyUsed(err): + loadKeysData(ctx) + ctx.Data["Err_Content"] = true ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) case models.IsErrGPGNoEmailFound(err): + loadKeysData(ctx) + ctx.Data["Err_Content"] = true ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) default: @@ -103,9 +83,13 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) { ctx.Data["HasSSHError"] = true switch { case models.IsErrKeyAlreadyExist(err): + loadKeysData(ctx) + ctx.Data["Err_Content"] = true ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) case models.IsErrKeyNameAlreadyUsed(err): + loadKeysData(ctx) + ctx.Data["Err_Title"] = true ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) default: @@ -147,3 +131,19 @@ func DeleteKey(ctx *context.Context) { "redirect": setting.AppSubURL + "/user/settings/keys", }) } + +func loadKeysData(ctx *context.Context) { + keys, err := models.ListPublicKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListPublicKeys", err) + return + } + ctx.Data["Keys"] = keys + + gpgkeys, err := models.ListGPGKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListGPGKeys", err) + return + } + ctx.Data["GPGKeys"] = gpgkeys +} diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go index cf222d002..22511ab89 100644 --- a/routers/user/setting/profile.go +++ b/routers/user/setting/profile.go @@ -32,6 +32,7 @@ const ( func Profile(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsProfile"] = true + ctx.HTML(200, tplSettingsProfile) } diff --git a/routers/user/setting/security.go b/routers/user/setting/security.go index 860730303..862e4413c 100644 --- a/routers/user/setting/security.go +++ b/routers/user/setting/security.go @@ -22,6 +22,30 @@ func Security(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true + if ctx.Query("openid.return_to") != "" { + settingsOpenIDVerify(ctx) + return + } + + loadSecurityData(ctx) + + ctx.HTML(200, tplSettingsSecurity) +} + +// DeleteAccountLink delete a single account link +func DeleteAccountLink(ctx *context.Context) { + if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { + ctx.Flash.Error("RemoveAccountLink: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) + } + + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/security", + }) +} + +func loadSecurityData(ctx *context.Context) { enrolled := true _, err := models.GetTwoFactorByUID(ctx.User.ID) if err != nil { @@ -71,30 +95,10 @@ func Security(ctx *context.Context) { } ctx.Data["AccountLinks"] = sources - if ctx.Query("openid.return_to") != "" { - settingsOpenIDVerify(ctx) - return - } - openid, err := models.GetUserOpenIDs(ctx.User.ID) if err != nil { ctx.ServerError("GetUserOpenIDs", err) return } ctx.Data["OpenIDs"] = openid - - ctx.HTML(200, tplSettingsSecurity) -} - -// DeleteAccountLink delete a single account link -func DeleteAccountLink(ctx *context.Context) { - if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { - ctx.Flash.Error("RemoveAccountLink: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) - } - - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/security", - }) } diff --git a/routers/user/setting/security_openid.go b/routers/user/setting/security_openid.go index c98dc2cda..6813765f6 100644 --- a/routers/user/setting/security_openid.go +++ b/routers/user/setting/security_openid.go @@ -19,12 +19,8 @@ func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { ctx.Data["PageIsSettingsSecurity"] = true if ctx.HasError() { - openid, err := models.GetUserOpenIDs(ctx.User.ID) - if err != nil { - ctx.ServerError("GetUserOpenIDs", err) - return - } - ctx.Data["OpenIDs"] = openid + loadSecurityData(ctx) + ctx.HTML(200, tplSettingsSecurity) return } @@ -37,6 +33,8 @@ func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { id, err := openid.Normalize(form.Openid) if err != nil { + loadSecurityData(ctx) + ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) return } @@ -53,6 +51,8 @@ func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { // Check that the OpenID is not already used for _, obj := range oids { if obj.URI == id { + loadSecurityData(ctx) + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form) return } @@ -61,6 +61,8 @@ func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { redirectTo := setting.AppURL + "user/settings/security" url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) if err != nil { + loadSecurityData(ctx) + ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) return } @@ -73,13 +75,6 @@ func settingsOpenIDVerify(ctx *context.Context) { fullURL := setting.AppURL + ctx.Req.Request.URL.String()[1:] log.Trace("Full URL: " + fullURL) - oids, err := models.GetUserOpenIDs(ctx.User.ID) - if err != nil { - ctx.ServerError("GetUserOpenIDs", err) - return - } - ctx.Data["OpenIDs"] = oids - id, err := openid.Verify(fullURL) if err != nil { ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &auth.AddOpenIDForm{ diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl index ec1a3dd72..12cdd697c 100644 --- a/templates/org/team/new.tmpl +++ b/templates/org/team/new.tmpl @@ -57,7 +57,7 @@ {{range $t, $unit := $.Units}}
- + {{$.i18n.Tr $unit.DescKey}}