|
|
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
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/base"
|
|
|
|
"code.gitea.io/gitea/modules/git"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
|
|
"code.gitea.io/gitea/modules/markup"
|
|
|
|
"code.gitea.io/gitea/modules/optional"
|
|
|
|
"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"
|
|
|
|
|
|
|
|
"xorm.io/builder"
|
|
|
|
)
|
|
|
|
|
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>
2 years ago
|
|
|
// ErrUserDoesNotHaveAccessToRepo represents an error where the user doesn't has access to a given repo.
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (err ErrUserDoesNotHaveAccessToRepo) Unwrap() error {
|
|
|
|
return util.ErrPermissionDenied
|
|
|
|
}
|
|
|
|
|
|
|
|
type ErrRepoIsArchived struct {
|
|
|
|
Repo *Repository
|
|
|
|
}
|
|
|
|
|
|
|
|
func (err ErrRepoIsArchived) Error() string {
|
|
|
|
return fmt.Sprintf("%s is archived", err.Repo.LogString())
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
reservedRepoNames = []string{".", "..", "-"}
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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>
2 years ago
|
|
|
NumActionRuns int `xorm:"NOT NULL DEFAULT 0"`
|
|
|
|
NumClosedActionRuns int `xorm:"NOT NULL DEFAULT 0"`
|
|
|
|
NumOpenActionRuns int `xorm:"-"`
|
|
|
|
|
|
|
|
IsPrivate bool `xorm:"INDEX"`
|
|
|
|
IsEmpty bool `xorm:"INDEX"`
|
|
|
|
IsArchived bool `xorm:"INDEX"`
|
|
|
|
IsMirror bool `xorm:"INDEX"`
|
|
|
|
|
|
|
|
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"`
|
|
|
|
GitSize int64 `xorm:"NOT NULL DEFAULT 0"`
|
|
|
|
LFSSize 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"`
|
|
|
|
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
|
|
|
|
|
|
|
|
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"`
|
|
|
|
ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
db.RegisterModel(new(Repository))
|
|
|
|
}
|
|
|
|
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
10 months ago
|
|
|
func (repo *Repository) GetName() string {
|
|
|
|
return repo.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
func (repo *Repository) GetOwnerName() string {
|
|
|
|
return repo.OwnerName
|
|
|
|
}
|
|
|
|
|
|
|
|
// SanitizedOriginalURL returns a sanitized OriginalURL
|
|
|
|
func (repo *Repository) SanitizedOriginalURL() string {
|
|
|
|
if repo.OriginalURL == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
u, _ := util.SanitizeURL(repo.OriginalURL)
|
|
|
|
return u
|
|
|
|
}
|
|
|
|
|
|
|
|
// text representations to be returned in SizeDetail.Name
|
|
|
|
const (
|
|
|
|
SizeDetailNameGit = "git"
|
|
|
|
SizeDetailNameLFS = "lfs"
|
|
|
|
)
|
|
|
|
|
|
|
|
type SizeDetail struct {
|
|
|
|
Name string
|
|
|
|
Size int64
|
|
|
|
}
|
|
|
|
|
|
|
|
// SizeDetails forms a struct with various size details about repository
|
|
|
|
func (repo *Repository) SizeDetails() []SizeDetail {
|
|
|
|
sizeDetails := []SizeDetail{
|
|
|
|
{
|
|
|
|
Name: SizeDetailNameGit,
|
|
|
|
Size: repo.GitSize,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: SizeDetailNameLFS,
|
|
|
|
Size: repo.LFSSize,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return sizeDetails
|
|
|
|
}
|
|
|
|
|
|
|
|
// SizeDetailsString returns a concatenation of all repository size details as a string
|
|
|
|
func (repo *Repository) SizeDetailsString() string {
|
|
|
|
var str strings.Builder
|
|
|
|
sizeDetails := repo.SizeDetails()
|
|
|
|
for _, detail := range sizeDetails {
|
|
|
|
str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size)))
|
|
|
|
}
|
|
|
|
return strings.TrimSuffix(str.String(), ", ")
|
|
|
|
}
|
|
|
|
|
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
|
|
|
func (repo *Repository) LogString() string {
|
|
|
|
if repo == nil {
|
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
|
|
|
return "<Repository nil>"
|
|
|
|
}
|
Rewrite logger system (#24726)
## ⚠️ Breaking
The `log.<mode>.<logger>` style config has been dropped. If you used it,
please check the new config manual & app.example.ini to make your
instance output logs as expected.
Although many legacy options still work, it's encouraged to upgrade to
the new options.
The SMTP logger is deleted because SMTP is not suitable to collect logs.
If you have manually configured Gitea log options, please confirm the
logger system works as expected after upgrading.
## Description
Close #12082 and maybe more log-related issues, resolve some related
FIXMEs in old code (which seems unfixable before)
Just like rewriting queue #24505 : make code maintainable, clear legacy
bugs, and add the ability to support more writers (eg: JSON, structured
log)
There is a new document (with examples): `logging-config.en-us.md`
This PR is safer than the queue rewriting, because it's just for
logging, it won't break other logic.
## The old problems
The logging system is quite old and difficult to maintain:
* Unclear concepts: Logger, NamedLogger, MultiChannelledLogger,
SubLogger, EventLogger, WriterLogger etc
* Some code is diffuclt to konw whether it is right:
`log.DelNamedLogger("console")` vs `log.DelNamedLogger(log.DEFAULT)` vs
`log.DelLogger("console")`
* The old system heavily depends on ini config system, it's difficult to
create new logger for different purpose, and it's very fragile.
* The "color" trick is difficult to use and read, many colors are
unnecessary, and in the future structured log could help
* It's difficult to add other log formats, eg: JSON format
* The log outputer doesn't have full control of its goroutine, it's
difficult to make outputer have advanced behaviors
* The logs could be lost in some cases: eg: no Fatal error when using
CLI.
* Config options are passed by JSON, which is quite fragile.
* INI package makes the KEY in `[log]` section visible in `[log.sub1]`
and `[log.sub1.subA]`, this behavior is quite fragile and would cause
more unclear problems, and there is no strong requirement to support
`log.<mode>.<logger>` syntax.
## The new design
See `logger.go` for documents.
## Screenshot
<details>
![image](https://github.com/go-gitea/gitea/assets/2114189/4462d713-ba39-41f5-bb08-de912e67e1ff)
![image](https://github.com/go-gitea/gitea/assets/2114189/b188035e-f691-428b-8b2d-ff7b2199b2f9)
![image](https://github.com/go-gitea/gitea/assets/2114189/132e9745-1c3b-4e00-9e0d-15eaea495dee)
</details>
## TODO
* [x] add some new tests
* [x] fix some tests
* [x] test some sub-commands (manually ....)
---------
Co-authored-by: Jason Song <i@wolfogre.com>
Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: Giteabot <teabot@gitea.io>
2 years ago
|
|
|
return fmt.Sprintf("<Repository %d:%s/%s>", 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarkAsBrokenEmpty marks the repo as broken and empty
|
|
|
|
func (repo *Repository) MarkAsBrokenEmpty() {
|
|
|
|
repo.Status = RepositoryBroken
|
|
|
|
repo.IsEmpty = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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>
2 years ago
|
|
|
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadAttributes loads attributes of the repository.
|
|
|
|
func (repo *Repository) LoadAttributes(ctx context.Context) error {
|
|
|
|
// Load owner
|
|
|
|
if err := repo.LoadOwner(ctx); err != nil {
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 git.IsEmptyCommitID(commitID) {
|
|
|
|
result = ""
|
|
|
|
} else {
|
|
|
|
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
|
|
|
repo.Units, err = getUnitsByRepoID(ctx, repo.ID)
|
|
|
|
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, ", "))
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnitEnabled if this repository has the given unit enabled
|
|
|
|
func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
|
|
|
|
if err := repo.LoadUnits(ctx); err != nil {
|
|
|
|
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
|
|
|
|
func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit {
|
|
|
|
ru, err := repo.GetUnit(ctx, tp)
|
|
|
|
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),
|
|
|
|
}
|
|
|
|
} else if tp == unit.TypeActions {
|
|
|
|
return &RepoUnit{
|
|
|
|
Type: tp,
|
|
|
|
Config: new(ActionsConfig),
|
|
|
|
}
|
|
|
|
} else if tp == unit.TypeProjects {
|
|
|
|
return &RepoUnit{
|
|
|
|
Type: tp,
|
|
|
|
Config: new(ProjectsConfig),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &RepoUnit{
|
|
|
|
Type: tp,
|
|
|
|
Config: new(UnitConfig),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetUnit returns a RepoUnit object
|
|
|
|
func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, error) {
|
|
|
|
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}
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadOwner loads owner user
|
|
|
|
func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
|
|
|
|
if repo.Owner != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
repo.Owner, err = user_model.GetUserByID(ctx, repo.OwnerID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
|
|
|
if err := repo.LoadOwner(ctx); err != nil {
|
|
|
|
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(ctx context.Context) map[string]string {
|
|
|
|
if len(repo.RenderingMetas) == 0 {
|
|
|
|
metas := map[string]string{
|
|
|
|
"user": repo.OwnerName,
|
|
|
|
"repo": repo.Name,
|
|
|
|
"repoPath": repo.RepoPath(),
|
|
|
|
"mode": "comment",
|
|
|
|
}
|
|
|
|
|
|
|
|
unit, err := repo.GetUnit(ctx, unit.TypeExternalTracker)
|
|
|
|
if err == nil {
|
|
|
|
metas["format"] = unit.ExternalTrackerConfig().ExternalTrackerFormat
|
|
|
|
switch unit.ExternalTrackerConfig().ExternalTrackerStyle {
|
|
|
|
case markup.IssueNameStyleAlphanumeric:
|
|
|
|
metas["style"] = markup.IssueNameStyleAlphanumeric
|
|
|
|
case markup.IssueNameStyleRegexp:
|
|
|
|
metas["style"] = markup.IssueNameStyleRegexp
|
|
|
|
metas["regexp"] = unit.ExternalTrackerConfig().ExternalTrackerRegexpPattern
|
|
|
|
default:
|
|
|
|
metas["style"] = markup.IssueNameStyleNumeric
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
repo.MustOwner(ctx)
|
|
|
|
if repo.Owner.IsOrganization() {
|
|
|
|
teams := make([]string, 0, 5)
|
|
|
|
_ = db.GetEngine(ctx).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(ctx context.Context) map[string]string {
|
|
|
|
if len(repo.DocumentRenderingMetas) == 0 {
|
|
|
|
metas := map[string]string{}
|
|
|
|
for k, v := range repo.ComposeMetas(ctx) {
|
|
|
|
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)
|
|
|
|
func (repo *Repository) GetBaseRepo(ctx context.Context) (err error) {
|
|
|
|
if !repo.IsFork {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
repo.BaseRepo, err = GetRepositoryByID(ctx, repo.ForkID)
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Link returns the repository relative url
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string {
|
|
|
|
if baseRepo == nil {
|
|
|
|
baseRepo = repo
|
|
|
|
}
|
|
|
|
var cmpBranchEscaped string
|
|
|
|
if repo.ID != baseRepo.ID {
|
|
|
|
cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name))
|
|
|
|
}
|
|
|
|
cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName))
|
|
|
|
return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(ctx context.Context) bool {
|
|
|
|
return repo.CanEnablePulls() && repo.UnitEnabled(ctx, unit.TypePullRequests)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
func (repo *Repository) DescriptionHTML(ctx context.Context) template.HTML {
|
|
|
|
desc, err := markup.RenderDescriptionHTML(&markup.RenderContext{
|
|
|
|
Ctx: ctx,
|
|
|
|
// Don't use Metas to speedup requests
|
|
|
|
}, 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.SanitizeDescription(repo.Description))
|
|
|
|
}
|
|
|
|
return template.HTML(markup.SanitizeDescription(desc))
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 ComposeSSHCloneURL(ownerName, repoName string) string {
|
|
|
|
sshUser := setting.SSH.User
|
|
|
|
sshDomain := setting.SSH.Domain
|
|
|
|
|
|
|
|
// non-standard port, it must use full URI
|
|
|
|
if setting.SSH.Port != 22 {
|
|
|
|
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
|
|
|
|
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
|
|
|
}
|
|
|
|
|
|
|
|
// for standard port, it can use a shorter URI (without the port)
|
|
|
|
sshHost := sshDomain
|
|
|
|
if ip := net.ParseIP(sshHost); ip != nil && ip.To4() == nil {
|
|
|
|
sshHost = "[" + sshHost + "]" // for IPv6 address, wrap it with brackets
|
|
|
|
}
|
|
|
|
if setting.Repository.UseCompatSSHURI {
|
|
|
|
return fmt.Sprintf("ssh://%s@%s/%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
|
|
|
|
repoName := repo.Name
|
|
|
|
if isWiki {
|
|
|
|
repoName += ".wiki"
|
|
|
|
}
|
|
|
|
|
|
|
|
cl := new(CloneLink)
|
|
|
|
cl.SSH = ComposeSSHCloneURL(repo.OwnerName, 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
|
|
|
|
}
|
|
|
|
|
|
|
|
// MustNotBeArchived returns ErrRepoIsArchived if the repo is archived
|
|
|
|
func (repo *Repository) MustNotBeArchived() error {
|
|
|
|
if repo.IsArchived {
|
|
|
|
return ErrRepoIsArchived{Repo: repo}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// __________ .__ __
|
|
|
|
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
|
|
|
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
|
|
|
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
|
|
|
|
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
|
|
|
|
// \/ \/|__| \/ \/
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unwrap unwraps this error as a ErrNotExist error
|
|
|
|
func (err ErrRepoNotExist) Unwrap() error {
|
|
|
|
return util.ErrNotExist
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetRepositoryByOwnerAndName returns the repository by given owner name and repo name
|
|
|
|
func GetRepositoryByOwnerAndName(ctx context.Context, ownerName, repoName string) (*Repository, error) {
|
|
|
|
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(ctx context.Context, ownerID int64, name string) (*Repository, error) {
|
|
|
|
repo := &Repository{
|
|
|
|
OwnerID: ownerID,
|
|
|
|
LowerName: strings.ToLower(name),
|
|
|
|
}
|
|
|
|
has, err := db.GetEngine(ctx).Get(repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if !has {
|
|
|
|
return nil, ErrRepoNotExist{0, ownerID, "", name}
|
|
|
|
}
|
|
|
|
return repo, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetRepositoryByID returns the repository by given id if exists.
|
|
|
|
func GetRepositoryByID(ctx context.Context, id int64) (*Repository, error) {
|
|
|
|
repo := new(Repository)
|
|
|
|
has, err := db.GetEngine(ctx).ID(id).Get(repo)
|
|
|
|
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(ctx context.Context, ids []int64) (map[int64]*Repository, error) {
|
|
|
|
repos := make(map[int64]*Repository, len(ids))
|
|
|
|
return repos, db.GetEngine(ctx).In("id", ids).Find(&repos)
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed.
|
|
|
|
func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
|
|
|
|
has, err := IsRepositoryModelExist(ctx, u, repoName)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
isDir, err := util.IsDir(RepoPath(u.Name, repoName))
|
|
|
|
return has || isDir, err
|
|
|
|
}
|
|
|
|
|
|
|
|
func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) {
|
|
|
|
return db.GetEngine(ctx).Get(&Repository{
|
|
|
|
OwnerID: u.ID,
|
|
|
|
LowerName: strings.ToLower(repoName),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) {
|
|
|
|
if !repo.IsGenerated() {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return GetRepositoryByID(ctx, repo.TemplateID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TemplateRepo returns the repository, which is template of this repository
|
|
|
|
func (repo *Repository) TemplateRepo(ctx context.Context) *Repository {
|
|
|
|
repo, err := GetTemplateRepo(ctx, repo)
|
|
|
|
if err != nil {
|
|
|
|
log.Error("TemplateRepo: %v", err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return repo
|
|
|
|
}
|
|
|
|
|
|
|
|
type CountRepositoryOptions struct {
|
|
|
|
OwnerID int64
|
|
|
|
Private optional.Option[bool]
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
if opts.Private.Has() {
|
|
|
|
sess.And("is_private=?", opts.Private.Value())
|
|
|
|
}
|
|
|
|
|
|
|
|
count, err := sess.Count(new(Repository))
|
|
|
|
if err != nil {
|
|
|
|
return 0, fmt.Errorf("countRepositories: %w", err)
|
|
|
|
}
|
|
|
|
return count, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateRepoIssueNumbers updates one of a repositories amount of (open|closed) (issues|PRs) with the current count
|
|
|
|
func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed bool) error {
|
|
|
|
field := "num_"
|
|
|
|
if isClosed {
|
|
|
|
field += "closed_"
|
|
|
|
}
|
|
|
|
if isPull {
|
|
|
|
field += "pulls"
|
|
|
|
} else {
|
|
|
|
field += "issues"
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// CountNullArchivedRepository counts the number of repositories with is_archived is null
|
|
|
|
func CountNullArchivedRepository(ctx context.Context) (int64, error) {
|
|
|
|
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
|
|
|
|
}
|
|
|
|
|
|
|
|
// FixNullArchivedRepository sets is_archived to false where it is null
|
|
|
|
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
|
|
|
|
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{
|
|
|
|
IsArchived: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateRepositoryOwnerName updates the owner name of all repositories owned by the user
|
|
|
|
func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName string) error {
|
|
|
|
if _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil {
|
|
|
|
return fmt.Errorf("change repo owner name: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|