2021-12-10 04:27:50 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2021-12-10 04:27:50 +03:00
package repo
import (
"context"
"fmt"
"html/template"
"net"
"net/url"
"path/filepath"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
2022-08-25 05:31:57 +03:00
"xorm.io/builder"
2021-12-10 04:27:50 +03:00
)
Fix various typos (#21103)
Found via `codespell -q 3 -S
./options/locale,./options/license,./public/vendor,./web_src/fomantic -L
actived,allways,attachements,ba,befores,commiter,pullrequest,pullrequests,readby,splitted,te,unknwon`
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
2022-09-07 21:40:36 +03:00
// ErrUserDoesNotHaveAccessToRepo represents an error where the user doesn't has access to a given repo.
2022-06-13 12:37:59 +03:00
type ErrUserDoesNotHaveAccessToRepo struct {
UserID int64
RepoName string
}
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
func IsErrUserDoesNotHaveAccessToRepo ( err error ) bool {
_ , ok := err . ( ErrUserDoesNotHaveAccessToRepo )
return ok
}
func ( err ErrUserDoesNotHaveAccessToRepo ) Error ( ) string {
return fmt . Sprintf ( "user doesn't have access to repo [user_id: %d, repo_name: %s]" , err . UserID , err . RepoName )
}
2022-10-18 08:50:37 +03:00
func ( err ErrUserDoesNotHaveAccessToRepo ) Unwrap ( ) error {
return util . ErrPermissionDenied
}
2021-12-12 18:48:20 +03:00
var (
2022-03-30 11:42:47 +03:00
reservedRepoNames = [ ] string { "." , ".." , "-" }
2021-12-12 18:48:20 +03:00
reservedRepoPatterns = [ ] string { "*.git" , "*.wiki" , "*.rss" , "*.atom" }
)
// IsUsableRepoName returns true when repository is usable
func IsUsableRepoName ( name string ) error {
if db . AlphaDashDotPattern . MatchString ( name ) {
// Note: usually this error is normally caught up earlier in the UI
return db . ErrNameCharsNotAllowed { Name : name }
}
return db . IsUsableName ( reservedRepoNames , reservedRepoPatterns , name )
}
2021-12-10 04:27:50 +03:00
// TrustModelType defines the types of trust model for this repository
type TrustModelType int
// kinds of TrustModel
const (
DefaultTrustModel TrustModelType = iota // default trust model
CommitterTrustModel
CollaboratorTrustModel
CollaboratorCommitterTrustModel
)
// String converts a TrustModelType to a string
func ( t TrustModelType ) String ( ) string {
switch t {
case DefaultTrustModel :
return "default"
case CommitterTrustModel :
return "committer"
case CollaboratorTrustModel :
return "collaborator"
case CollaboratorCommitterTrustModel :
return "collaboratorcommitter"
}
return "default"
}
// ToTrustModel converts a string to a TrustModelType
func ToTrustModel ( model string ) TrustModelType {
switch strings . ToLower ( strings . TrimSpace ( model ) ) {
case "default" :
return DefaultTrustModel
case "collaborator" :
return CollaboratorTrustModel
case "committer" :
return CommitterTrustModel
case "collaboratorcommitter" :
return CollaboratorCommitterTrustModel
}
return DefaultTrustModel
}
// RepositoryStatus defines the status of repository
type RepositoryStatus int
// all kinds of RepositoryStatus
const (
RepositoryReady RepositoryStatus = iota // a normal repository
RepositoryBeingMigrated // repository is migrating
RepositoryPendingTransfer // repository pending in ownership transfer state
RepositoryBroken // repository is in a permanently broken state
)
// Repository represents a git repository.
type Repository struct {
ID int64 ` xorm:"pk autoincr" `
OwnerID int64 ` xorm:"UNIQUE(s) index" `
OwnerName string
Owner * user_model . User ` xorm:"-" `
LowerName string ` xorm:"UNIQUE(s) INDEX NOT NULL" `
Name string ` xorm:"INDEX NOT NULL" `
Description string ` xorm:"TEXT" `
Website string ` xorm:"VARCHAR(2048)" `
OriginalServiceType api . GitServiceType ` xorm:"index" `
OriginalURL string ` xorm:"VARCHAR(2048)" `
DefaultBranch string
NumWatches int
NumStars int
NumForks int
NumIssues int
NumClosedIssues int
NumOpenIssues int ` xorm:"-" `
NumPulls int
NumClosedPulls int
NumOpenPulls int ` xorm:"-" `
NumMilestones int ` xorm:"NOT NULL DEFAULT 0" `
NumClosedMilestones int ` xorm:"NOT NULL DEFAULT 0" `
NumOpenMilestones int ` xorm:"-" `
NumProjects int ` xorm:"NOT NULL DEFAULT 0" `
NumClosedProjects int ` xorm:"NOT NULL DEFAULT 0" `
NumOpenProjects int ` xorm:"-" `
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 04:45:19 +03:00
NumActionRuns int ` xorm:"NOT NULL DEFAULT 0" `
NumClosedActionRuns int ` xorm:"NOT NULL DEFAULT 0" `
NumOpenActionRuns int ` xorm:"-" `
2021-12-10 04:27:50 +03:00
IsPrivate bool ` xorm:"INDEX" `
IsEmpty bool ` xorm:"INDEX" `
IsArchived bool ` xorm:"INDEX" `
IsMirror bool ` xorm:"INDEX" `
* Mirror ` xorm:"-" `
Status RepositoryStatus ` xorm:"NOT NULL DEFAULT 0" `
RenderingMetas map [ string ] string ` xorm:"-" `
DocumentRenderingMetas map [ string ] string ` xorm:"-" `
Units [ ] * RepoUnit ` xorm:"-" `
PrimaryLanguage * LanguageStat ` xorm:"-" `
IsFork bool ` xorm:"INDEX NOT NULL DEFAULT false" `
ForkID int64 ` xorm:"INDEX" `
BaseRepo * Repository ` xorm:"-" `
IsTemplate bool ` xorm:"INDEX NOT NULL DEFAULT false" `
TemplateID int64 ` xorm:"INDEX" `
Size int64 ` xorm:"NOT NULL DEFAULT 0" `
CodeIndexerStatus * RepoIndexerStatus ` xorm:"-" `
StatsIndexerStatus * RepoIndexerStatus ` xorm:"-" `
IsFsckEnabled bool ` xorm:"NOT NULL DEFAULT true" `
CloseIssuesViaCommitInAnyBranch bool ` xorm:"NOT NULL DEFAULT false" `
Topics [ ] string ` xorm:"TEXT JSON" `
TrustModel TrustModelType
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
Avatar string ` xorm:"VARCHAR(64)" `
CreatedUnix timeutil . TimeStamp ` xorm:"INDEX created" `
UpdatedUnix timeutil . TimeStamp ` xorm:"INDEX updated" `
}
func init ( ) {
db . RegisterModel ( new ( Repository ) )
}
// SanitizedOriginalURL returns a sanitized OriginalURL
func ( repo * Repository ) SanitizedOriginalURL ( ) string {
if repo . OriginalURL == "" {
return ""
}
u , err := url . Parse ( repo . OriginalURL )
if err != nil {
return ""
}
u . User = nil
return u . String ( )
}
// ColorFormat returns a colored string to represent this repo
func ( repo * Repository ) ColorFormat ( s fmt . State ) {
2021-12-16 22:01:14 +03:00
if repo == nil {
log . ColorFprintf ( s , "%d:%s/%s" ,
log . NewColoredIDValue ( 0 ) ,
"<nil>" ,
"<nil>" )
return
}
2021-12-10 04:27:50 +03:00
log . ColorFprintf ( s , "%d:%s/%s" ,
log . NewColoredIDValue ( repo . ID ) ,
repo . OwnerName ,
repo . Name )
}
// IsBeingMigrated indicates that repository is being migrated
func ( repo * Repository ) IsBeingMigrated ( ) bool {
return repo . Status == RepositoryBeingMigrated
}
// IsBeingCreated indicates that repository is being migrated or forked
func ( repo * Repository ) IsBeingCreated ( ) bool {
return repo . IsBeingMigrated ( )
}
// IsBroken indicates that repository is broken
func ( repo * Repository ) IsBroken ( ) bool {
return repo . Status == RepositoryBroken
}
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
func ( repo * Repository ) AfterLoad ( ) {
repo . NumOpenIssues = repo . NumIssues - repo . NumClosedIssues
repo . NumOpenPulls = repo . NumPulls - repo . NumClosedPulls
repo . NumOpenMilestones = repo . NumMilestones - repo . NumClosedMilestones
repo . NumOpenProjects = repo . NumProjects - repo . NumClosedProjects
Implement actions (#21937)
Close #13539.
Co-authored by: @lunny @appleboy @fuxiaohei and others.
Related projects:
- https://gitea.com/gitea/actions-proto-def
- https://gitea.com/gitea/actions-proto-go
- https://gitea.com/gitea/act
- https://gitea.com/gitea/act_runner
### Summary
The target of this PR is to bring a basic implementation of "Actions",
an internal CI/CD system of Gitea. That means even though it has been
merged, the state of the feature is **EXPERIMENTAL**, and please note
that:
- It is disabled by default;
- It shouldn't be used in a production environment currently;
- It shouldn't be used in a public Gitea instance currently;
- Breaking changes may be made before it's stable.
**Please comment on #13539 if you have any different product design
ideas**, all decisions reached there will be adopted here. But in this
PR, we don't talk about **naming, feature-creep or alternatives**.
### ⚠️ Breaking
`gitea-actions` will become a reserved user name. If a user with the
name already exists in the database, it is recommended to rename it.
### Some important reviews
- What is `DEFAULT_ACTIONS_URL` in `app.ini` for?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1055954954
- Why the api for runners is not under the normal `/api/v1` prefix?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061173592
- Why DBFS?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1061301178
- Why ignore events triggered by `gitea-actions` bot?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1063254103
- Why there's no permission control for actions?
- https://github.com/go-gitea/gitea/pull/21937#discussion_r1090229868
### What it looks like
<details>
#### Manage runners
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205870657-c72f590e-2e08-4cd4-be7f-2e0abb299bbf.png">
#### List runs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872794-50fde990-2b45-48c1-a178-908e4ec5b627.png">
#### View logs
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205872501-9b7b9000-9542-4991-8f55-18ccdada77c3.png">
</details>
### How to try it
<details>
#### 1. Start Gitea
Clone this branch and [install from
source](https://docs.gitea.io/en-us/install-from-source).
Add additional configurations in `app.ini` to enable Actions:
```ini
[actions]
ENABLED = true
```
Start it.
If all is well, you'll see the management page of runners:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205877365-8e30a780-9b10-4154-b3e8-ee6c3cb35a59.png">
#### 2. Start runner
Clone the [act_runner](https://gitea.com/gitea/act_runner), and follow
the
[README](https://gitea.com/gitea/act_runner/src/branch/main/README.md)
to start it.
If all is well, you'll see a new runner has been added:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205878000-216f5937-e696-470d-b66c-8473987d91c3.png">
#### 3. Enable actions for a repo
Create a new repo or open an existing one, check the `Actions` checkbox
in settings and submit.
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879705-53e09208-73c0-4b3e-a123-2dcf9aba4b9c.png">
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205879383-23f3d08f-1a85-41dd-a8b3-54e2ee6453e8.png">
If all is well, you'll see a new tab "Actions":
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205881648-a8072d8c-5803-4d76-b8a8-9b2fb49516c1.png">
#### 4. Upload workflow files
Upload some workflow files to `.gitea/workflows/xxx.yaml`, you can
follow the [quickstart](https://docs.github.com/en/actions/quickstart)
of GitHub Actions. Yes, Gitea Actions is compatible with GitHub Actions
in most cases, you can use the same demo:
```yaml
name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ github.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
```
If all is well, you'll see a new run in `Actions` tab:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884473-79a874bc-171b-4aaf-acd5-0241a45c3b53.png">
#### 5. Check the logs of jobs
Click a run and you'll see the logs:
<img width="1792" alt="image"
src="https://user-images.githubusercontent.com/9418365/205884800-994b0374-67f7-48ff-be9a-4c53f3141547.png">
#### 6. Go on
You can try more examples in [the
documents](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
of GitHub Actions, then you might find a lot of bugs.
Come on, PRs are welcome.
</details>
See also: [Feature Preview: Gitea
Actions](https://blog.gitea.io/2022/12/feature-preview-gitea-actions/)
---------
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: ChristopherHX <christopher.homberger@web.de>
Co-authored-by: John Olheiser <john.olheiser@gmail.com>
2023-01-31 04:45:19 +03:00
repo . NumOpenActionRuns = repo . NumActionRuns - repo . NumClosedActionRuns
2021-12-10 04:27:50 +03:00
}
2022-01-25 09:33:40 +03:00
// LoadAttributes loads attributes of the repository.
func ( repo * Repository ) LoadAttributes ( ctx context . Context ) error {
// Load owner
2023-02-18 15:11:03 +03:00
if err := repo . LoadOwner ( ctx ) ; err != nil {
2022-01-25 09:33:40 +03:00
return fmt . Errorf ( "load owner: %w" , err )
}
// Load primary language
stats := make ( LanguageStatList , 0 , 1 )
if err := db . GetEngine ( ctx ) .
Where ( "`repo_id` = ? AND `is_primary` = ? AND `language` != ?" , repo . ID , true , "other" ) .
Find ( & stats ) ; err != nil {
return fmt . Errorf ( "find primary languages: %w" , err )
}
stats . LoadAttributes ( )
for _ , st := range stats {
if st . RepoID == repo . ID {
repo . PrimaryLanguage = st
break
}
}
return nil
}
2021-12-10 04:27:50 +03:00
// FullName returns the repository full name
func ( repo * Repository ) FullName ( ) string {
return repo . OwnerName + "/" + repo . Name
}
// HTMLURL returns the repository HTML URL
func ( repo * Repository ) HTMLURL ( ) string {
return setting . AppURL + url . PathEscape ( repo . OwnerName ) + "/" + url . PathEscape ( repo . Name )
}
// CommitLink make link to by commit full ID
// note: won't check whether it's an right id
func ( repo * Repository ) CommitLink ( commitID string ) ( result string ) {
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
result = ""
} else {
2023-02-11 09:34:11 +03:00
result = repo . Link ( ) + "/commit/" + url . PathEscape ( commitID )
2021-12-10 04:27:50 +03:00
}
2022-06-20 13:02:49 +03:00
return result
2021-12-10 04:27:50 +03:00
}
// APIURL returns the repository API URL
func ( repo * Repository ) APIURL ( ) string {
return setting . AppURL + "api/v1/repos/" + url . PathEscape ( repo . OwnerName ) + "/" + url . PathEscape ( repo . Name )
}
// GetCommitsCountCacheKey returns cache key used for commits count caching.
func ( repo * Repository ) GetCommitsCountCacheKey ( contextName string , isRef bool ) string {
var prefix string
if isRef {
prefix = "ref"
} else {
prefix = "commit"
}
return fmt . Sprintf ( "commits-count-%d-%s-%s" , repo . ID , prefix , contextName )
}
// LoadUnits loads repo units into repo.Units
func ( repo * Repository ) LoadUnits ( ctx context . Context ) ( err error ) {
if repo . Units != nil {
return nil
}
2022-05-20 17:08:52 +03:00
repo . Units , err = getUnitsByRepoID ( ctx , repo . ID )
2022-02-27 18:49:22 +03:00
if log . IsTrace ( ) {
unitTypeStrings := make ( [ ] string , len ( repo . Units ) )
for i , unit := range repo . Units {
unitTypeStrings [ i ] = unit . Type . String ( )
}
log . Trace ( "repo.Units, ID=%d, Types: [%s]" , repo . ID , strings . Join ( unitTypeStrings , ", " ) )
}
2021-12-10 04:27:50 +03:00
return err
}
// UnitEnabled if this repository has the given unit enabled
2022-12-10 05:46:31 +03:00
func ( repo * Repository ) UnitEnabled ( ctx context . Context , tp unit . Type ) bool {
2022-05-03 22:46:28 +03:00
if err := repo . LoadUnits ( ctx ) ; err != nil {
2021-12-10 04:27:50 +03:00
log . Warn ( "Error loading repository (ID: %d) units: %s" , repo . ID , err . Error ( ) )
}
for _ , unit := range repo . Units {
if unit . Type == tp {
return true
}
}
return false
}
// MustGetUnit always returns a RepoUnit object
2022-12-10 05:46:31 +03:00
func ( repo * Repository ) MustGetUnit ( ctx context . Context , tp unit . Type ) * RepoUnit {
ru , err := repo . GetUnit ( ctx , tp )
2021-12-10 04:27:50 +03:00
if err == nil {
return ru
}
if tp == unit . TypeExternalWiki {
return & RepoUnit {
Type : tp ,
Config : new ( ExternalWikiConfig ) ,
}
} else if tp == unit . TypeExternalTracker {
return & RepoUnit {
Type : tp ,
Config : new ( ExternalTrackerConfig ) ,
}
} else if tp == unit . TypePullRequests {
return & RepoUnit {
Type : tp ,
Config : new ( PullRequestsConfig ) ,
}
} else if tp == unit . TypeIssues {
return & RepoUnit {
Type : tp ,
Config : new ( IssuesConfig ) ,
}
}
return & RepoUnit {
Type : tp ,
Config : new ( UnitConfig ) ,
}
}
// GetUnit returns a RepoUnit object
2022-12-10 05:46:31 +03:00
func ( repo * Repository ) GetUnit ( ctx context . Context , tp unit . Type ) ( * RepoUnit , error ) {
2021-12-10 04:27:50 +03:00
if err := repo . LoadUnits ( ctx ) ; err != nil {
return nil , err
}
for _ , unit := range repo . Units {
if unit . Type == tp {
return unit , nil
}
}
return nil , ErrUnitTypeNotExist { tp }
}
2023-02-18 15:11:03 +03:00
// LoadOwner loads owner user
func ( repo * Repository ) LoadOwner ( ctx context . Context ) ( err error ) {
2021-12-10 04:27:50 +03:00
if repo . Owner != nil {
return nil
}
2022-12-03 05:48:26 +03:00
repo . Owner , err = user_model . GetUserByID ( ctx , repo . OwnerID )
2021-12-10 04:27:50 +03:00
return err
}
2022-11-19 11:12:33 +03:00
// MustOwner always returns a valid *user_model.User object to avoid
// conceptually impossible error handling.
// It creates a fake object that contains error details
// when error occurs.
func ( repo * Repository ) MustOwner ( ctx context . Context ) * user_model . User {
2023-02-18 15:11:03 +03:00
if err := repo . LoadOwner ( ctx ) ; err != nil {
2021-12-10 04:27:50 +03:00
return & user_model . User {
Name : "error" ,
FullName : err . Error ( ) ,
}
}
return repo . Owner
}
// ComposeMetas composes a map of metas for properly rendering issue links and external issue trackers.
func ( repo * Repository ) ComposeMetas ( ) map [ string ] string {
if len ( repo . RenderingMetas ) == 0 {
metas := map [ string ] string {
"user" : repo . OwnerName ,
"repo" : repo . Name ,
"repoPath" : repo . RepoPath ( ) ,
"mode" : "comment" ,
}
2022-12-10 05:46:31 +03:00
unit , err := repo . GetUnit ( db . DefaultContext , unit . TypeExternalTracker )
2021-12-10 04:27:50 +03:00
if err == nil {
metas [ "format" ] = unit . ExternalTrackerConfig ( ) . ExternalTrackerFormat
switch unit . ExternalTrackerConfig ( ) . ExternalTrackerStyle {
case markup . IssueNameStyleAlphanumeric :
metas [ "style" ] = markup . IssueNameStyleAlphanumeric
2022-06-10 08:39:53 +03:00
case markup . IssueNameStyleRegexp :
metas [ "style" ] = markup . IssueNameStyleRegexp
metas [ "regexp" ] = unit . ExternalTrackerConfig ( ) . ExternalTrackerRegexpPattern
2021-12-10 04:27:50 +03:00
default :
metas [ "style" ] = markup . IssueNameStyleNumeric
}
}
2022-11-19 11:12:33 +03:00
repo . MustOwner ( db . DefaultContext )
2021-12-10 04:27:50 +03:00
if repo . Owner . IsOrganization ( ) {
teams := make ( [ ] string , 0 , 5 )
_ = db . GetEngine ( db . DefaultContext ) . Table ( "team_repo" ) .
Join ( "INNER" , "team" , "team.id = team_repo.team_id" ) .
Where ( "team_repo.repo_id = ?" , repo . ID ) .
Select ( "team.lower_name" ) .
OrderBy ( "team.lower_name" ) .
Find ( & teams )
metas [ "teams" ] = "," + strings . Join ( teams , "," ) + ","
metas [ "org" ] = strings . ToLower ( repo . OwnerName )
}
repo . RenderingMetas = metas
}
return repo . RenderingMetas
}
// ComposeDocumentMetas composes a map of metas for properly rendering documents
func ( repo * Repository ) ComposeDocumentMetas ( ) map [ string ] string {
if len ( repo . DocumentRenderingMetas ) == 0 {
metas := map [ string ] string { }
for k , v := range repo . ComposeMetas ( ) {
metas [ k ] = v
}
metas [ "mode" ] = "document"
repo . DocumentRenderingMetas = metas
}
return repo . DocumentRenderingMetas
}
// GetBaseRepo populates repo.BaseRepo for a fork repository and
// returns an error on failure (NOTE: no error is returned for
// non-fork repositories, and BaseRepo will be left untouched)
2022-12-03 05:48:26 +03:00
func ( repo * Repository ) GetBaseRepo ( ctx context . Context ) ( err error ) {
2021-12-10 04:27:50 +03:00
if ! repo . IsFork {
return nil
}
2022-12-03 05:48:26 +03:00
repo . BaseRepo , err = GetRepositoryByID ( ctx , repo . ForkID )
2021-12-10 04:27:50 +03:00
return err
}
// IsGenerated returns whether _this_ repository was generated from a template
func ( repo * Repository ) IsGenerated ( ) bool {
return repo . TemplateID != 0
}
// RepoPath returns repository path by given user and repository name.
func RepoPath ( userName , repoName string ) string { //revive:disable-line:exported
return filepath . Join ( user_model . UserPath ( userName ) , strings . ToLower ( repoName ) + ".git" )
}
// RepoPath returns the repository path
func ( repo * Repository ) RepoPath ( ) string {
return RepoPath ( repo . OwnerName , repo . Name )
}
2023-02-06 21:09:18 +03:00
// Link returns the repository relative url
2021-12-10 04:27:50 +03:00
func ( repo * Repository ) Link ( ) string {
return setting . AppSubURL + "/" + url . PathEscape ( repo . OwnerName ) + "/" + url . PathEscape ( repo . Name )
}
// ComposeCompareURL returns the repository comparison URL
func ( repo * Repository ) ComposeCompareURL ( oldCommitID , newCommitID string ) string {
return fmt . Sprintf ( "%s/%s/compare/%s...%s" , url . PathEscape ( repo . OwnerName ) , url . PathEscape ( repo . Name ) , util . PathEscapeSegments ( oldCommitID ) , util . PathEscapeSegments ( newCommitID ) )
}
// IsOwnedBy returns true when user owns this repository
func ( repo * Repository ) IsOwnedBy ( userID int64 ) bool {
return repo . OwnerID == userID
}
// CanCreateBranch returns true if repository meets the requirements for creating new branches.
func ( repo * Repository ) CanCreateBranch ( ) bool {
return ! repo . IsMirror
}
// CanEnablePulls returns true if repository meets the requirements of accepting pulls.
func ( repo * Repository ) CanEnablePulls ( ) bool {
return ! repo . IsMirror && ! repo . IsEmpty
}
// AllowsPulls returns true if repository meets the requirements of accepting pulls and has them enabled.
func ( repo * Repository ) AllowsPulls ( ) bool {
2022-12-10 05:46:31 +03:00
return repo . CanEnablePulls ( ) && repo . UnitEnabled ( db . DefaultContext , unit . TypePullRequests )
2021-12-10 04:27:50 +03:00
}
// CanEnableEditor returns true if repository meets the requirements of web editor.
func ( repo * Repository ) CanEnableEditor ( ) bool {
return ! repo . IsMirror
}
// DescriptionHTML does special handles to description and return HTML string.
2022-01-20 02:26:57 +03:00
func ( repo * Repository ) DescriptionHTML ( ctx context . Context ) template . HTML {
2021-12-10 04:27:50 +03:00
desc , err := markup . RenderDescriptionHTML ( & markup . RenderContext {
2022-01-20 02:26:57 +03:00
Ctx : ctx ,
2021-12-10 04:27:50 +03:00
URLPrefix : repo . HTMLURL ( ) ,
2022-02-27 20:51:34 +03:00
// Don't use Metas to speedup requests
2021-12-10 04:27:50 +03:00
} , repo . Description )
if err != nil {
log . Error ( "Failed to render description for %s (ID: %d): %v" , repo . Name , repo . ID , err )
return template . HTML ( markup . Sanitize ( repo . Description ) )
}
2022-06-20 13:02:49 +03:00
return template . HTML ( markup . Sanitize ( desc ) )
2021-12-10 04:27:50 +03:00
}
// CloneLink represents different types of clone URLs of repository.
type CloneLink struct {
SSH string
HTTPS string
}
// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
func ComposeHTTPSCloneURL ( owner , repo string ) string {
return fmt . Sprintf ( "%s%s/%s.git" , setting . AppURL , url . PathEscape ( owner ) , url . PathEscape ( repo ) )
}
func ( repo * Repository ) cloneLink ( isWiki bool ) * CloneLink {
repoName := repo . Name
if isWiki {
repoName += ".wiki"
}
2022-02-08 00:56:45 +03:00
sshUser := setting . SSH . User
2021-12-10 04:27:50 +03:00
cl := new ( CloneLink )
// if we have a ipv6 literal we need to put brackets around it
// for the git cloning to work.
sshDomain := setting . SSH . Domain
ip := net . ParseIP ( setting . SSH . Domain )
if ip != nil && ip . To4 ( ) == nil {
sshDomain = "[" + setting . SSH . Domain + "]"
}
if setting . SSH . Port != 22 {
cl . SSH = fmt . Sprintf ( "ssh://%s@%s/%s/%s.git" , sshUser , net . JoinHostPort ( setting . SSH . Domain , strconv . Itoa ( setting . SSH . Port ) ) , url . PathEscape ( repo . OwnerName ) , url . PathEscape ( repoName ) )
} else if setting . Repository . UseCompatSSHURI {
cl . SSH = fmt . Sprintf ( "ssh://%s@%s/%s/%s.git" , sshUser , sshDomain , url . PathEscape ( repo . OwnerName ) , url . PathEscape ( repoName ) )
} else {
cl . SSH = fmt . Sprintf ( "%s@%s:%s/%s.git" , sshUser , sshDomain , url . PathEscape ( repo . OwnerName ) , url . PathEscape ( repoName ) )
}
cl . HTTPS = ComposeHTTPSCloneURL ( repo . OwnerName , repoName )
return cl
}
// CloneLink returns clone URLs of repository.
func ( repo * Repository ) CloneLink ( ) ( cl * CloneLink ) {
return repo . cloneLink ( false )
}
// GetOriginalURLHostname returns the hostname of a URL or the URL
func ( repo * Repository ) GetOriginalURLHostname ( ) string {
u , err := url . Parse ( repo . OriginalURL )
if err != nil {
return repo . OriginalURL
}
return u . Host
}
// GetTrustModel will get the TrustModel for the repo or the default trust model
func ( repo * Repository ) GetTrustModel ( ) TrustModelType {
trustModel := repo . TrustModel
if trustModel == DefaultTrustModel {
trustModel = ToTrustModel ( setting . Repository . Signing . DefaultTrustModel )
if trustModel == DefaultTrustModel {
return CollaboratorTrustModel
}
}
return trustModel
}
// __________ .__ __
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
// \/ \/|__| \/ \/
// ErrRepoNotExist represents a "RepoNotExist" kind of error.
type ErrRepoNotExist struct {
ID int64
UID int64
OwnerName string
Name string
}
// IsErrRepoNotExist checks if an error is a ErrRepoNotExist.
func IsErrRepoNotExist ( err error ) bool {
_ , ok := err . ( ErrRepoNotExist )
return ok
}
func ( err ErrRepoNotExist ) Error ( ) string {
return fmt . Sprintf ( "repository does not exist [id: %d, uid: %d, owner_name: %s, name: %s]" ,
err . ID , err . UID , err . OwnerName , err . Name )
}
2022-10-18 08:50:37 +03:00
// Unwrap unwraps this error as a ErrNotExist error
func ( err ErrRepoNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2022-12-03 05:48:26 +03:00
// GetRepositoryByOwnerAndName returns the repository by given owner name and repo name
func GetRepositoryByOwnerAndName ( ctx context . Context , ownerName , repoName string ) ( * Repository , error ) {
2021-12-10 04:27:50 +03:00
var repo Repository
has , err := db . GetEngine ( ctx ) . Table ( "repository" ) . Select ( "repository.*" ) .
Join ( "INNER" , "`user`" , "`user`.id = repository.owner_id" ) .
Where ( "repository.lower_name = ?" , strings . ToLower ( repoName ) ) .
And ( "`user`.lower_name = ?" , strings . ToLower ( ownerName ) ) .
Get ( & repo )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrRepoNotExist { 0 , 0 , ownerName , repoName }
}
return & repo , nil
}
// GetRepositoryByName returns the repository by given name under user if exists.
func GetRepositoryByName ( ownerID int64 , name string ) ( * Repository , error ) {
repo := & Repository {
OwnerID : ownerID ,
LowerName : strings . ToLower ( name ) ,
}
has , err := db . GetEngine ( db . DefaultContext ) . Get ( repo )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrRepoNotExist { 0 , ownerID , "" , name }
}
return repo , err
}
2023-03-28 20:55:03 +03:00
// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
func getRepositoryURLPathSegments ( repoURL string ) [ ] string {
if strings . HasPrefix ( repoURL , setting . AppURL ) {
return strings . Split ( strings . TrimPrefix ( repoURL , setting . AppURL ) , "/" )
}
sshURLVariants := [ 4 ] string {
setting . SSH . Domain + ":" ,
setting . SSH . User + "@" + setting . SSH . Domain + ":" ,
"git+ssh://" + setting . SSH . Domain + "/" ,
"git+ssh://" + setting . SSH . User + "@" + setting . SSH . Domain + "/" ,
}
for _ , sshURL := range sshURLVariants {
if strings . HasPrefix ( repoURL , sshURL ) {
return strings . Split ( strings . TrimPrefix ( repoURL , sshURL ) , "/" )
}
}
return nil
}
// GetRepositoryByURL returns the repository by given url
func GetRepositoryByURL ( ctx context . Context , repoURL string ) ( * Repository , error ) {
// possible urls for git:
// https://my.domain/sub-path/<owner>/<repo>.git
// https://my.domain/sub-path/<owner>/<repo>
// git+ssh://user@my.domain/<owner>/<repo>.git
// git+ssh://user@my.domain/<owner>/<repo>
// user@my.domain:<owner>/<repo>.git
// user@my.domain:<owner>/<repo>
pathSegments := getRepositoryURLPathSegments ( repoURL )
if len ( pathSegments ) != 2 {
return nil , fmt . Errorf ( "unknown or malformed repository URL" )
}
ownerName := pathSegments [ 0 ]
repoName := strings . TrimSuffix ( pathSegments [ 1 ] , ".git" )
return GetRepositoryByOwnerAndName ( ctx , ownerName , repoName )
}
2022-12-03 05:48:26 +03:00
// GetRepositoryByID returns the repository by given id if exists.
func GetRepositoryByID ( ctx context . Context , id int64 ) ( * Repository , error ) {
2021-12-10 04:27:50 +03:00
repo := new ( Repository )
2022-05-20 17:08:52 +03:00
has , err := db . GetEngine ( ctx ) . ID ( id ) . Get ( repo )
2021-12-10 04:27:50 +03:00
if err != nil {
return nil , err
} else if ! has {
return nil , ErrRepoNotExist { id , 0 , "" , "" }
}
return repo , nil
}
// GetRepositoriesMapByIDs returns the repositories by given id slice.
func GetRepositoriesMapByIDs ( ids [ ] int64 ) ( map [ int64 ] * Repository , error ) {
repos := make ( map [ int64 ] * Repository , len ( ids ) )
return repos , db . GetEngine ( db . DefaultContext ) . In ( "id" , ids ) . Find ( & repos )
}
2022-05-20 17:08:52 +03:00
// IsRepositoryExist returns true if the repository with given name under user has already existed.
func IsRepositoryExist ( ctx context . Context , u * user_model . User , repoName string ) ( bool , error ) {
2021-12-10 04:27:50 +03:00
has , err := db . GetEngine ( ctx ) . Get ( & Repository {
OwnerID : u . ID ,
LowerName : strings . ToLower ( repoName ) ,
} )
if err != nil {
return false , err
}
isDir , err := util . IsDir ( RepoPath ( u . Name , repoName ) )
return has && isDir , err
}
// GetTemplateRepo populates repo.TemplateRepo for a generated repository and
// returns an error on failure (NOTE: no error is returned for
// non-generated repositories, and TemplateRepo will be left untouched)
2022-05-20 17:08:52 +03:00
func GetTemplateRepo ( ctx context . Context , repo * Repository ) ( * Repository , error ) {
2021-12-10 04:27:50 +03:00
if ! repo . IsGenerated ( ) {
return nil , nil
}
2022-12-03 05:48:26 +03:00
return GetRepositoryByID ( ctx , repo . TemplateID )
2021-12-10 04:27:50 +03:00
}
2021-12-16 10:12:50 +03:00
// TemplateRepo returns the repository, which is template of this repository
func ( repo * Repository ) TemplateRepo ( ) * Repository {
2022-05-20 17:08:52 +03:00
repo , err := GetTemplateRepo ( db . DefaultContext , repo )
2021-12-16 10:12:50 +03:00
if err != nil {
log . Error ( "TemplateRepo: %v" , err )
return nil
}
return repo
}
2022-05-20 17:08:52 +03:00
type CountRepositoryOptions struct {
OwnerID int64
Private util . OptionalBool
}
2021-12-10 04:27:50 +03:00
2022-05-20 17:08:52 +03:00
// CountRepositories returns number of repositories.
// Argument private only takes effect when it is false,
// set it true to count all repositories.
func CountRepositories ( ctx context . Context , opts CountRepositoryOptions ) ( int64 , error ) {
sess := db . GetEngine ( ctx ) . Where ( "id > 0" )
if opts . OwnerID > 0 {
sess . And ( "owner_id = ?" , opts . OwnerID )
2021-12-10 04:27:50 +03:00
}
2022-05-20 17:08:52 +03:00
if ! opts . Private . IsNone ( ) {
sess . And ( "is_private=?" , opts . Private . IsTrue ( ) )
2021-12-10 04:27:50 +03:00
}
count , err := sess . Count ( new ( Repository ) )
if err != nil {
2022-10-24 22:29:17 +03:00
return 0 , fmt . Errorf ( "countRepositories: %w" , err )
2021-12-10 04:27:50 +03:00
}
2022-05-20 17:08:52 +03:00
return count , nil
2021-12-10 04:27:50 +03:00
}
2022-06-13 12:37:59 +03:00
2022-10-25 15:47:46 +03:00
// UpdateRepoIssueNumbers updates one of a repositories amount of (open|closed) (issues|PRs) with the current count
2022-06-13 12:37:59 +03:00
func UpdateRepoIssueNumbers ( ctx context . Context , repoID int64 , isPull , isClosed bool ) error {
2022-10-25 15:47:46 +03:00
field := "num_"
if isClosed {
field += "closed_"
}
2022-06-13 12:37:59 +03:00
if isPull {
2022-10-25 15:47:46 +03:00
field += "pulls"
2022-06-13 12:37:59 +03:00
} else {
2022-10-25 15:47:46 +03:00
field += "issues"
2022-06-13 12:37:59 +03:00
}
2022-10-25 15:47:46 +03:00
subQuery := builder . Select ( "count(*)" ) .
From ( "issue" ) . Where ( builder . Eq {
"repo_id" : repoID ,
"is_pull" : isPull ,
} . And ( builder . If ( isClosed , builder . Eq { "is_closed" : isClosed } ) ) )
// builder.Update(cond) will generate SQL like UPDATE ... SET cond
query := builder . Update ( builder . Eq { field : subQuery } ) .
From ( "repository" ) .
Where ( builder . Eq { "id" : repoID } )
_ , err := db . Exec ( ctx , query )
return err
2022-06-13 12:37:59 +03:00
}
2022-08-25 05:31:57 +03:00
// CountNullArchivedRepository counts the number of repositories with is_archived is null
2022-11-19 11:12:33 +03:00
func CountNullArchivedRepository ( ctx context . Context ) ( int64 , error ) {
return db . GetEngine ( ctx ) . Where ( builder . IsNull { "is_archived" } ) . Count ( new ( Repository ) )
2022-08-25 05:31:57 +03:00
}
// FixNullArchivedRepository sets is_archived to false where it is null
2022-11-19 11:12:33 +03:00
func FixNullArchivedRepository ( ctx context . Context ) ( int64 , error ) {
return db . GetEngine ( ctx ) . Where ( builder . IsNull { "is_archived" } ) . Cols ( "is_archived" ) . NoAutoTime ( ) . Update ( & Repository {
2022-08-25 05:31:57 +03:00
IsArchived : false ,
} )
}