mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			91 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8cfd6695da | ||
| 
						 | 
					f832e8eeea | ||
| 
						 | 
					544ef7d394 | ||
| 
						 | 
					5ff807acde | ||
| 
						 | 
					849d316d8d | ||
| 
						 | 
					946eb1321c | ||
| 
						 | 
					bc82bb9cda | ||
| 
						 | 
					f034804e5d | ||
| 
						 | 
					c1887bfc9b | ||
| 
						 | 
					41a4047e79 | ||
| 
						 | 
					ac84bb7183 | ||
| 
						 | 
					3be67e9a2b | ||
| 
						 | 
					ce2ade05e6 | ||
| 
						 | 
					1e76f7b5b7 | ||
| 
						 | 
					2265058c31 | ||
| 
						 | 
					ba74fdbda9 | ||
| 
						 | 
					0600f7972a | ||
| 
						 | 
					8007602b40 | ||
| 
						 | 
					3a79f1190f | ||
| 
						 | 
					d95489b7ed | ||
| 
						 | 
					a9e1a37b71 | ||
| 
						 | 
					5a589ef9ec | ||
| 
						 | 
					159bc8842a | ||
| 
						 | 
					4b771d393e | ||
| 
						 | 
					0c2cbfcb3b | ||
| 
						 | 
					8c4bf4c3b4 | ||
| 
						 | 
					3bcf2e5c18 | ||
| 
						 | 
					ad54f008ac | ||
| 
						 | 
					c21167e3a2 | ||
| 
						 | 
					aaa539dd2d | ||
| 
						 | 
					e38134f707 | ||
| 
						 | 
					fa96ddb327 | ||
| 
						 | 
					a3e8450fd5 | ||
| 
						 | 
					41422f0df0 | ||
| 
						 | 
					f773733252 | ||
| 
						 | 
					cbaf8e8785 | ||
| 
						 | 
					1bf46836da | ||
| 
						 | 
					387a1bc472 | ||
| 
						 | 
					62daf84596 | ||
| 
						 | 
					39d209dccc | ||
| 
						 | 
					c88392e772 | ||
| 
						 | 
					a83cde2f3f | ||
| 
						 | 
					332eb2f6d2 | ||
| 
						 | 
					3ae1d7a59f | ||
| 
						 | 
					d054c4e7f3 | ||
| 
						 | 
					5e562e9b30 | ||
| 
						 | 
					c57e908f36 | ||
| 
						 | 
					1112fef93d | ||
| 
						 | 
					af11549fb2 | ||
| 
						 | 
					76d6184cd0 | ||
| 
						 | 
					d644709b22 | ||
| 
						 | 
					30584a6df8 | ||
| 
						 | 
					78710946f2 | ||
| 
						 | 
					22d700edfd | ||
| 
						 | 
					6782a64a4a | ||
| 
						 | 
					1ec11ac87e | ||
| 
						 | 
					2c2a30d6bb | ||
| 
						 | 
					717b313c34 | ||
| 
						 | 
					0a32861b28 | ||
| 
						 | 
					52ca7b9b65 | ||
| 
						 | 
					e078d08ecd | ||
| 
						 | 
					a83fb3a83a | ||
| 
						 | 
					f9b1fac4ea | ||
| 
						 | 
					f1e8b8c0d7 | ||
| 
						 | 
					dbbb75712d | ||
| 
						 | 
					462c6fdee2 | ||
| 
						 | 
					cead819cb5 | ||
| 
						 | 
					4fa2804238 | ||
| 
						 | 
					3ce46a7fbd | ||
| 
						 | 
					15886ce048 | ||
| 
						 | 
					a725d31496 | ||
| 
						 | 
					8e27f6e814 | ||
| 
						 | 
					54263ff123 | ||
| 
						 | 
					3bde297121 | ||
| 
						 | 
					0dfde367c1 | ||
| 
						 | 
					875501584b | ||
| 
						 | 
					4190c134e6 | ||
| 
						 | 
					cae46216e4 | ||
| 
						 | 
					761111f9ed | ||
| 
						 | 
					57f1476093 | ||
| 
						 | 
					bdba89452d | ||
| 
						 | 
					6e2dacfef6 | ||
| 
						 | 
					c0869c295a | ||
| 
						 | 
					a719311f6d | ||
| 
						 | 
					248b67af6f | ||
| 
						 | 
					990c6089db | ||
| 
						 | 
					5da024a019 | ||
| 
						 | 
					eff2499be7 | ||
| 
						 | 
					4a3c6384ac | ||
| 
						 | 
					2b1989e59f | ||
| 
						 | 
					340c4fc7c7 | 
@@ -522,7 +522,7 @@ steps:
 | 
			
		||||
    image: plugins/s3:1
 | 
			
		||||
    settings:
 | 
			
		||||
      acl: public-read
 | 
			
		||||
      bucket: releases
 | 
			
		||||
      bucket: gitea-artifacts
 | 
			
		||||
      endpoint: https://storage.gitea.io
 | 
			
		||||
      path_style: true
 | 
			
		||||
      source: "dist/release/*"
 | 
			
		||||
@@ -543,7 +543,7 @@ steps:
 | 
			
		||||
    image: plugins/s3:1
 | 
			
		||||
    settings:
 | 
			
		||||
      acl: public-read
 | 
			
		||||
      bucket: releases
 | 
			
		||||
      bucket: gitea-artifacts
 | 
			
		||||
      endpoint: https://storage.gitea.io
 | 
			
		||||
      path_style: true
 | 
			
		||||
      source: "dist/release/*"
 | 
			
		||||
@@ -618,7 +618,7 @@ steps:
 | 
			
		||||
    image: plugins/s3:1
 | 
			
		||||
    settings:
 | 
			
		||||
      acl: public-read
 | 
			
		||||
      bucket: releases
 | 
			
		||||
      bucket: gitea-artifacts
 | 
			
		||||
      endpoint: https://storage.gitea.io
 | 
			
		||||
      path_style: true
 | 
			
		||||
      source: "dist/release/*"
 | 
			
		||||
 
 | 
			
		||||
@@ -110,3 +110,7 @@ issues:
 | 
			
		||||
    - text: "exitAfterDefer:"
 | 
			
		||||
      linters:
 | 
			
		||||
        - gocritic
 | 
			
		||||
    - path: modules/graceful/manager_windows.go
 | 
			
		||||
      linters:
 | 
			
		||||
        - staticcheck
 | 
			
		||||
      text: "svc.IsAnInteractiveSession is deprecated: Use IsWindowsService instead."
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										100
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,6 +4,106 @@ This changelog goes through all the changes that have been made in each release
 | 
			
		||||
without substantial changes to our git log; to see the highlights of what has
 | 
			
		||||
been added to each release, please refer to the [blog](https://blog.gitea.io).
 | 
			
		||||
 | 
			
		||||
## [1.14.3](https://github.com/go-gitea/gitea/releases/tag/v1.14.3) - 2021-06-10
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Encrypt migration credentials at rest (#15895) (#16187)
 | 
			
		||||
  * Only check access tokens if they are likely to be tokens (#16164) (#16171)
 | 
			
		||||
  * Add missing SameSite settings for the i_like_gitea cookie (#16037) (#16039)
 | 
			
		||||
  * Fix setting of SameSite on cookies (#15989) (#15991)
 | 
			
		||||
* API
 | 
			
		||||
  * Repository object only count releases as releases (#16184) (#16190)
 | 
			
		||||
  * EditOrg respect RepoAdminChangeTeamAccess option (#16184) (#16190)
 | 
			
		||||
  * Fix overly strict edit pr permissions (#15900) (#16081)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Run processors on whole of text (#16155) (#16185)
 | 
			
		||||
  * Class `issue-keyword` is being incorrectly stripped off spans (#16163) (#16172)
 | 
			
		||||
  * Fix language switch for install page (#16043) (#16128)
 | 
			
		||||
  * Fix bug on getIssueIDsByRepoID (#16119) (#16124)
 | 
			
		||||
  * Set self-adjusting deadline for connection writing (#16068) (#16123)
 | 
			
		||||
  * Fix http path bug (#16117) (#16120)
 | 
			
		||||
  * Fix data URI scramble (#16098) (#16118)
 | 
			
		||||
  * Merge all deleteBranch as one function and also fix bug when delete branch don't close related PRs (#16067) (#16097)
 | 
			
		||||
  * git migration: don't prompt interactively for clone credentials (#15902) (#16082)
 | 
			
		||||
  * Fix case change in ownernames (#16045) (#16050)
 | 
			
		||||
  * Don't manipulate input params in email notification (#16011) (#16033)
 | 
			
		||||
  * Remove branch URL before IssueRefURL (#15968) (#15970)
 | 
			
		||||
  * Fix layout of milestone view (#15927) (#15940)
 | 
			
		||||
  * GitHub Migration, migrate draft releases too (#15884) (#15888)
 | 
			
		||||
  * Close the gitrepo when deleting the repository (#15876) (#15887)
 | 
			
		||||
  * Upgrade xorm to v1.1.0 (#15869) (#15885)
 | 
			
		||||
  * Fix blame row height alignment (#15863) (#15883)
 | 
			
		||||
  * Fix error message when saving generated LOCAL_ROOT_URL config (#15880) (#15882)
 | 
			
		||||
  * Backport Fix LFS commit finder not working (#15856) (#15874)
 | 
			
		||||
  * Stop calling WriteHeader in Write (#15862) (#15873)
 | 
			
		||||
  * Add timeout to writing to responses (#15831) (#15872)
 | 
			
		||||
  * Return go-get info on subdirs (#15642) (#15871)
 | 
			
		||||
  * Restore PAM user autocreation functionality (#15825) (#15867)
 | 
			
		||||
  * Fix truncate utf8 string (#15828) (#15854)
 | 
			
		||||
  * Fix bound address/port for caddy's certmagic library (#15758) (#15848)
 | 
			
		||||
  * Upgrade unrolled/render to v1.1.1 (#15845) (#15846)
 | 
			
		||||
  * Queue manager FlushAll can loop rapidly - add delay (#15733) (#15840)
 | 
			
		||||
  * Tagger can be empty, as can Commit and Author - tolerate this (#15835) (#15839)
 | 
			
		||||
  * Set autocomplete off on branches selector (#15809) (#15833)
 | 
			
		||||
  * Add missing error to Doctor log (#15813) (#15824)
 | 
			
		||||
  * Move restore repo to internal router and invoke from command to avoid open the same db file or queues files (#15790) (#15816)
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Removable media support to snap package (#16136) (#16138)
 | 
			
		||||
  * Move sans-serif fallback font higher than emoji fonts (#15855) (#15892)
 | 
			
		||||
* DOCKER
 | 
			
		||||
  * Only write config in environment-to-ini if there are changes (#15861) (#15868)
 | 
			
		||||
  * Only offer hostcertificates if they exist (#15849) (#15853)
 | 
			
		||||
 | 
			
		||||
## [1.14.2](https://github.com/go-gitea/gitea/releases/tag/v1.14.2) - 2021-05-08
 | 
			
		||||
 | 
			
		||||
* API
 | 
			
		||||
  * Make change repo settings work on empty repos (#15778) (#15789)
 | 
			
		||||
  * Add pull "merged" notification subject status to API (#15344) (#15654)
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Ensure that ctx.Written is checked after issues(...) calls (#15797) (#15798)
 | 
			
		||||
  * Use pulls in commit graph unless pulls are disabled (#15734 & #15740 & #15774) (#15775)
 | 
			
		||||
  * Set GIT_DIR correctly if it is not set (#15751) (#15769)
 | 
			
		||||
  * Fix bug where repositories appear unadopted (#15757) (#15767)
 | 
			
		||||
  * Not show `ref-in-new-issue` pop when issue was disabled (#15761) (#15765)
 | 
			
		||||
  * Drop back to use IsAnInteractiveSession for SVC (#15749) (#15762)
 | 
			
		||||
  * Fix setting version table in dump (#15753) (#15759)
 | 
			
		||||
  * Fix close button change on delete in simplemde area (#15737) (#15747)
 | 
			
		||||
  * Defer closing the gitrepo until the end of the wrapped context functions (#15653) (#15746)
 | 
			
		||||
  * Fix some ui bug about draft release (#15137) (#15745)
 | 
			
		||||
  * Only log Error on getLastCommitStatus error to let pull list still be visible (#15716) (#15715)
 | 
			
		||||
  * Move tooltip down to allow selection of Remove File on error (#15672) (#15714)
 | 
			
		||||
  * Fix setting redis db path (#15698) (#15708)
 | 
			
		||||
  * Fix DB session cleanup (#15697) (#15700)
 | 
			
		||||
  * Fixed several activation bugs (#15473) (#15685)
 | 
			
		||||
  * Delete references if repository gets deleted (#15681) (#15684)
 | 
			
		||||
  * Fix orphaned objects deletion bug (#15657) (#15683)
 | 
			
		||||
  * Delete protected branch if repository gets removed (#15658) (#15676)
 | 
			
		||||
  * Remove spurious set name from eventsource.sharedworker.js (#15643) (#15652)
 | 
			
		||||
  * Not update updated uinx for `git gc` (#15637) (#15641)
 | 
			
		||||
  * Fix commit graph author link (#15627) (#15630)
 | 
			
		||||
  * Fix webhook timeout bug (#15613) (#15621)
 | 
			
		||||
  * Resolve panic on failed interface conversion in migration v156 (#15604) (#15610)
 | 
			
		||||
  * Fix missing storage init (#15589) (#15598)
 | 
			
		||||
  * If the default branch is not present do not report error on stats indexing (#15546 & #15583) (#15594)
 | 
			
		||||
  * Fix lfs management find (#15537) (#15578)
 | 
			
		||||
  * Fix NPE on view commit with notes (#15561) (#15573)
 | 
			
		||||
  * Fix bug on commit graph (#15517) (#15530)
 | 
			
		||||
  * Send size to /avatars if requested (#15459) (#15528)
 | 
			
		||||
  * Prevent migration 156 failure if tag commit missing (#15519) (#15527)
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Display conflict-free merge messages for pull requests (#15773) (#15796)
 | 
			
		||||
  * Exponential Backoff for ByteFIFO (#15724) (#15793)
 | 
			
		||||
  * Issue list alignment tweaks (#15483) (#15766)
 | 
			
		||||
  * Implement delete release attachments and update release attachments' name (#14130) (#15666)
 | 
			
		||||
  * Add placeholder text to deploy key textarea (#15575) (#15576)
 | 
			
		||||
  * Project board improvements (#15429) (#15560)
 | 
			
		||||
  * Repo branch page: label size, PR ref, new PR button alignment (#15363) (#15365)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Fix webkit calendar icon color on arc-green (#15713) (#15728)
 | 
			
		||||
  * Performance improvement for last commit cache and show-ref (#15455) (#15701)
 | 
			
		||||
  * Bump unrolled/render to v1.1.0 (#15581) (#15608)
 | 
			
		||||
  * Add ETag header (#15370) (#15552)
 | 
			
		||||
 | 
			
		||||
## [1.14.1](https://github.com/go-gitea/gitea/releases/tag/v1.14.1) - 2021-04-15
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	pwd "code.gitea.io/gitea/modules/password"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@@ -489,6 +490,10 @@ func runDeleteUser(c *cli.Context) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := storage.Init(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	var user *models.User
 | 
			
		||||
	if c.IsSet("email") {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,15 +5,12 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/migrations"
 | 
			
		||||
	"code.gitea.io/gitea/modules/migrations/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/storage"
 | 
			
		||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli"
 | 
			
		||||
)
 | 
			
		||||
@@ -50,70 +47,18 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func runRestoreRepository(ctx *cli.Context) error {
 | 
			
		||||
	if err := initDB(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	setting.NewContext()
 | 
			
		||||
 | 
			
		||||
	log.Trace("AppPath: %s", setting.AppPath)
 | 
			
		||||
	log.Trace("AppWorkPath: %s", setting.AppWorkPath)
 | 
			
		||||
	log.Trace("Custom path: %s", setting.CustomPath)
 | 
			
		||||
	log.Trace("Log path: %s", setting.LogRootPath)
 | 
			
		||||
	setting.InitDBConfig()
 | 
			
		||||
 | 
			
		||||
	if err := storage.Init(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pull_service.Init(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var opts = base.MigrateOptions{
 | 
			
		||||
		RepoName: ctx.String("repo_name"),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(ctx.String("units")) == 0 {
 | 
			
		||||
		opts.Wiki = true
 | 
			
		||||
		opts.Issues = true
 | 
			
		||||
		opts.Milestones = true
 | 
			
		||||
		opts.Labels = true
 | 
			
		||||
		opts.Releases = true
 | 
			
		||||
		opts.Comments = true
 | 
			
		||||
		opts.PullRequests = true
 | 
			
		||||
		opts.ReleaseAssets = true
 | 
			
		||||
	} else {
 | 
			
		||||
		units := strings.Split(ctx.String("units"), ",")
 | 
			
		||||
		for _, unit := range units {
 | 
			
		||||
			switch strings.ToLower(unit) {
 | 
			
		||||
			case "wiki":
 | 
			
		||||
				opts.Wiki = true
 | 
			
		||||
			case "issues":
 | 
			
		||||
				opts.Issues = true
 | 
			
		||||
			case "milestones":
 | 
			
		||||
				opts.Milestones = true
 | 
			
		||||
			case "labels":
 | 
			
		||||
				opts.Labels = true
 | 
			
		||||
			case "releases":
 | 
			
		||||
				opts.Releases = true
 | 
			
		||||
			case "release_assets":
 | 
			
		||||
				opts.ReleaseAssets = true
 | 
			
		||||
			case "comments":
 | 
			
		||||
				opts.Comments = true
 | 
			
		||||
			case "pull_requests":
 | 
			
		||||
				opts.PullRequests = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := migrations.RestoreRepository(
 | 
			
		||||
		context.Background(),
 | 
			
		||||
	statusCode, errStr := private.RestoreRepo(
 | 
			
		||||
		ctx.String("repo_dir"),
 | 
			
		||||
		ctx.String("owner_name"),
 | 
			
		||||
		ctx.String("repo_name"),
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		log.Fatal("Failed to restore repository: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
		ctx.StringSlice("units"),
 | 
			
		||||
	)
 | 
			
		||||
	if statusCode == http.StatusOK {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
	log.Fatal("Failed to restore repository: %v", errStr)
 | 
			
		||||
	return errors.New(errStr)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -175,7 +175,7 @@ func setPort(port string) error {
 | 
			
		||||
 | 
			
		||||
		cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL)
 | 
			
		||||
		if err := cfg.SaveTo(setting.CustomConf); err != nil {
 | 
			
		||||
			return fmt.Errorf("Error saving generated JWT Secret to custom config: %v", err)
 | 
			
		||||
			return fmt.Errorf("Error saving generated LOCAL_ROOT_URL to custom config: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -22,6 +23,15 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler)
 | 
			
		||||
	// TODO: these are placeholders until we add options for each in settings with appropriate warning
 | 
			
		||||
	enableHTTPChallenge := true
 | 
			
		||||
	enableTLSALPNChallenge := true
 | 
			
		||||
	altHTTPPort := 0
 | 
			
		||||
	altTLSALPNPort := 0
 | 
			
		||||
 | 
			
		||||
	if p, err := strconv.Atoi(setting.PortToRedirect); err == nil {
 | 
			
		||||
		altHTTPPort = p
 | 
			
		||||
	}
 | 
			
		||||
	if p, err := strconv.Atoi(setting.HTTPPort); err == nil {
 | 
			
		||||
		altTLSALPNPort = p
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	magic := certmagic.NewDefault()
 | 
			
		||||
	magic.Storage = &certmagic.FileStorage{Path: directory}
 | 
			
		||||
@@ -30,6 +40,9 @@ func runLetsEncrypt(listenAddr, domain, directory, email string, m http.Handler)
 | 
			
		||||
		Agreed:                  setting.LetsEncryptTOS,
 | 
			
		||||
		DisableHTTPChallenge:    !enableHTTPChallenge,
 | 
			
		||||
		DisableTLSALPNChallenge: !enableTLSALPNChallenge,
 | 
			
		||||
		ListenHost:              setting.HTTPAddr,
 | 
			
		||||
		AltTLSALPNPort:          altTLSALPNPort,
 | 
			
		||||
		AltHTTPPort:             altHTTPPort,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	magic.Issuer = myACME
 | 
			
		||||
 
 | 
			
		||||
@@ -110,6 +110,8 @@ func runEnvironmentToIni(c *cli.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
	cfg.NameMapper = ini.SnackCase
 | 
			
		||||
 | 
			
		||||
	changed := false
 | 
			
		||||
 | 
			
		||||
	prefix := c.String("prefix") + "__"
 | 
			
		||||
 | 
			
		||||
	for _, kv := range os.Environ() {
 | 
			
		||||
@@ -143,15 +145,21 @@ func runEnvironmentToIni(c *cli.Context) error {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		oldValue := key.Value()
 | 
			
		||||
		if !changed && oldValue != value {
 | 
			
		||||
			changed = true
 | 
			
		||||
		}
 | 
			
		||||
		key.SetValue(value)
 | 
			
		||||
	}
 | 
			
		||||
	destination := c.String("out")
 | 
			
		||||
	if len(destination) == 0 {
 | 
			
		||||
		destination = setting.CustomConf
 | 
			
		||||
	}
 | 
			
		||||
	err = cfg.SaveTo(destination)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	if destination != setting.CustomConf || changed {
 | 
			
		||||
		err = cfg.SaveTo(destination)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if c.Bool("clear") {
 | 
			
		||||
		for _, kv := range os.Environ() {
 | 
			
		||||
 
 | 
			
		||||
@@ -281,6 +281,10 @@ HTTP_PORT = 3000
 | 
			
		||||
; PORT_TO_REDIRECT.
 | 
			
		||||
REDIRECT_OTHER_PORT = false
 | 
			
		||||
PORT_TO_REDIRECT = 80
 | 
			
		||||
; Timeout for any write to the connection. (Set to 0 to disable all timeouts.)
 | 
			
		||||
PER_WRITE_TIMEOUT = 30s
 | 
			
		||||
; Timeout per Kb written to connections.
 | 
			
		||||
PER_WRITE_PER_KB_TIMEOUT = 30s
 | 
			
		||||
; Permission for unix socket
 | 
			
		||||
UNIX_SOCKET_PERMISSION = 666
 | 
			
		||||
; Local (DMZ) URL for Gitea workers (such as SSH update) accessing web service.
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,29 @@ if [ ! -f /data/ssh/ssh_host_ecdsa_key ]; then
 | 
			
		||||
    ssh-keygen -t ecdsa -b 256 -f /data/ssh/ssh_host_ecdsa_key -N "" > /dev/null
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -e /data/ssh/ssh_host_ed25519_cert ]; then
 | 
			
		||||
  SSH_ED25519_CERT=${SSH_ED25519_CERT:-"/data/ssh/ssh_host_ed25519_cert"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -e /data/ssh/ssh_host_rsa_cert ]; then
 | 
			
		||||
  SSH_RSA_CERT=${SSH_RSA_CERT:-"/data/ssh/ssh_host_rsa_cert"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -e /data/ssh/ssh_host_ecdsa_cert ]; then
 | 
			
		||||
  SSH_ECDSA_CERT=${SSH_ECDSA_CERT:-"/data/ssh/ssh_host_ecdsa_cert"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -e /data/ssh/ssh_host_dsa_cert ]; then
 | 
			
		||||
  SSH_DSA_CERT=${SSH_DSA_CERT:-"/data/ssh/ssh_host_dsa_cert"}
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if [ -d /etc/ssh ]; then
 | 
			
		||||
    SSH_PORT=${SSH_PORT:-"22"} \
 | 
			
		||||
    SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-"${SSH_PORT}"} \
 | 
			
		||||
    SSH_ED25519_CERT="${SSH_ED25519_CERT:+"HostCertificate "}${SSH_ED25519_CERT}" \
 | 
			
		||||
    SSH_RSA_CERT="${SSH_RSA_CERT:+"HostCertificate "}${SSH_RSA_CERT}" \
 | 
			
		||||
    SSH_ECDSA_CERT="${SSH_ECDSA_CERT:+"HostCertificate "}${SSH_ECDSA_CERT}" \
 | 
			
		||||
    SSH_DSA_CERT="${SSH_DSA_CERT:+"HostCertificate "}${SSH_DSA_CERT}" \
 | 
			
		||||
    envsubst < /etc/templates/sshd_config > /etc/ssh/sshd_config
 | 
			
		||||
 | 
			
		||||
    chmod 0644 /etc/ssh/sshd_config
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,13 @@ ListenAddress ::
 | 
			
		||||
LogLevel INFO
 | 
			
		||||
 | 
			
		||||
HostKey /data/ssh/ssh_host_ed25519_key
 | 
			
		||||
HostCertificate /data/ssh/ssh_host_ed25519_cert
 | 
			
		||||
${SSH_ED25519_CERT}
 | 
			
		||||
HostKey /data/ssh/ssh_host_rsa_key
 | 
			
		||||
HostCertificate /data/ssh/ssh_host_rsa_cert
 | 
			
		||||
${SSH_RSA_CERT}
 | 
			
		||||
HostKey /data/ssh/ssh_host_ecdsa_key
 | 
			
		||||
HostCertificate /data/ssh/ssh_host_ecdsa_cert
 | 
			
		||||
${SSH_ECDSA_CERT}
 | 
			
		||||
HostKey /data/ssh/ssh_host_dsa_key
 | 
			
		||||
HostCertificate /data/ssh/ssh_host_dsa_cert
 | 
			
		||||
${SSH_DSA_CERT}
 | 
			
		||||
 | 
			
		||||
AuthorizedKeysFile .ssh/authorized_keys
 | 
			
		||||
AuthorizedPrincipalsFile .ssh/authorized_principals
 | 
			
		||||
 
 | 
			
		||||
@@ -31,4 +31,4 @@ update: $(THEME)
 | 
			
		||||
$(THEME): $(THEME)/theme.toml
 | 
			
		||||
$(THEME)/theme.toml:
 | 
			
		||||
	mkdir -p $$(dirname $@)
 | 
			
		||||
	curl -s $(ARCHIVE) | tar xz -C $$(dirname $@)
 | 
			
		||||
	curl -L -s $(ARCHIVE) | tar xz -C $$(dirname $@)
 | 
			
		||||
 
 | 
			
		||||
@@ -237,6 +237,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		||||
   most cases you do not need to change the default value. Alter it only if
 | 
			
		||||
   your SSH server node is not the same as HTTP node. Do not set this variable
 | 
			
		||||
   if `PROTOCOL` is set to `unix`.
 | 
			
		||||
- `PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the connection. (Set to 0 to
 | 
			
		||||
   disable all timeouts.)
 | 
			
		||||
- `PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to connections.
 | 
			
		||||
 | 
			
		||||
- `DISABLE_SSH`: **false**: Disable SSH feature when it's not available.
 | 
			
		||||
- `START_SSH_SERVER`: **false**: When enabled, use the built-in SSH server.
 | 
			
		||||
@@ -260,6 +263,9 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		||||
- `SSH_KEY_TEST_PATH`: **/tmp**: Directory to create temporary files in when testing public keys using ssh-keygen, default is the system temporary directory.
 | 
			
		||||
- `SSH_KEYGEN_PATH`: **ssh-keygen**: Path to ssh-keygen, default is 'ssh-keygen' which means the shell is responsible for finding out which one to call.
 | 
			
		||||
- `SSH_EXPOSE_ANONYMOUS`: **false**: Enable exposure of SSH clone URL to anonymous visitors, default is false.
 | 
			
		||||
- `SSH_PER_WRITE_TIMEOUT`: **30s**: Timeout for any write to the SSH connections. (Set to
 | 
			
		||||
  0 to disable all timeouts.)
 | 
			
		||||
- `SSH_PER_WRITE_PER_KB_TIMEOUT`: **10s**: Timeout per Kb written to SSH connections.
 | 
			
		||||
- `MINIMUM_KEY_SIZE_CHECK`: **true**: Indicate whether to check minimum key size with corresponding type.
 | 
			
		||||
 | 
			
		||||
- `OFFLINE_MODE`: **false**: Disables use of CDN for static files and Gravatar for profile pictures.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							@@ -122,7 +122,7 @@ require (
 | 
			
		||||
	github.com/unknwon/com v1.0.1
 | 
			
		||||
	github.com/unknwon/i18n v0.0.0-20200823051745-09abd91c7f2c
 | 
			
		||||
	github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae
 | 
			
		||||
	github.com/unrolled/render v1.0.3
 | 
			
		||||
	github.com/unrolled/render v1.1.1
 | 
			
		||||
	github.com/urfave/cli v1.22.5
 | 
			
		||||
	github.com/willf/bitset v1.1.11 // indirect
 | 
			
		||||
	github.com/xanzy/go-gitlab v0.44.0
 | 
			
		||||
@@ -149,7 +149,7 @@ require (
 | 
			
		||||
	mvdan.cc/xurls/v2 v2.2.0
 | 
			
		||||
	strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251
 | 
			
		||||
	xorm.io/builder v0.3.9
 | 
			
		||||
	xorm.io/xorm v1.0.7
 | 
			
		||||
	xorm.io/xorm v1.1.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
replace github.com/hashicorp/go-version => github.com/6543/go-version v1.2.4
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								go.sum
									
									
									
									
									
								
							@@ -996,6 +996,8 @@ github.com/quasoft/websspi v1.0.0 h1:5nDgdM5xSur9s+B5w2xQ5kxf5nUGqgFgU4W0aDLZ8Mw
 | 
			
		||||
github.com/quasoft/websspi v1.0.0/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk=
 | 
			
		||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 | 
			
		||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 | 
			
		||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
			
		||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 | 
			
		||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
			
		||||
@@ -1113,8 +1115,8 @@ github.com/unknwon/i18n v0.0.0-20200823051745-09abd91c7f2c h1:679/gJXwrsHC3RATr0
 | 
			
		||||
github.com/unknwon/i18n v0.0.0-20200823051745-09abd91c7f2c/go.mod h1:+5rDk6sDGpl3azws3O+f+GpFSyN9GVr0K8cvQLQM2ZQ=
 | 
			
		||||
github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae h1:ihaXiJkaca54IaCSnEXtE/uSZOmPxKZhDfVLrzZLFDs=
 | 
			
		||||
github.com/unknwon/paginater v0.0.0-20200328080006-042474bd0eae/go.mod h1:1fdkY6xxl6ExVs2QFv7R0F5IRZHKA8RahhB9fMC9RvM=
 | 
			
		||||
github.com/unrolled/render v1.0.3 h1:baO+NG1bZSF2WR4zwh+0bMWauWky7DVrTOfvE2w+aFo=
 | 
			
		||||
github.com/unrolled/render v1.0.3/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
 | 
			
		||||
github.com/unrolled/render v1.1.1 h1:FpzNzkvlJQIlVdVaqeVBGWiCS8gpbmjtrKpDmCn6p64=
 | 
			
		||||
github.com/unrolled/render v1.1.1/go.mod h1:gN9T0NhL4Bfbwu8ann7Ry/TGHYfosul+J0obPf6NBdM=
 | 
			
		||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 | 
			
		||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 | 
			
		||||
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
 | 
			
		||||
@@ -1500,6 +1502,7 @@ golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4X
 | 
			
		||||
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201125231158-b5590deeca9b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
@@ -1666,6 +1669,33 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
 | 
			
		||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 | 
			
		||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
 | 
			
		||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 | 
			
		||||
modernc.org/cc/v3 v3.31.5-0.20210308123301-7a3e9dab9009 h1:u0oCo5b9wyLr++HF3AN9JicGhkUxJhMz51+8TIZH9N0=
 | 
			
		||||
modernc.org/cc/v3 v3.31.5-0.20210308123301-7a3e9dab9009/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878=
 | 
			
		||||
modernc.org/ccgo/v3 v3.9.0 h1:JbcEIqjw4Agf+0g3Tc85YvfYqkkFOv6xBwS4zkfqSoA=
 | 
			
		||||
modernc.org/ccgo/v3 v3.9.0/go.mod h1:nQbgkn8mwzPdp4mm6BT6+p85ugQ7FrGgIcYaE7nSrpY=
 | 
			
		||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
 | 
			
		||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
 | 
			
		||||
modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
 | 
			
		||||
modernc.org/libc v1.8.0 h1:Pp4uv9g0csgBMpGPABKtkieF6O5MGhfGo6ZiOdlYfR8=
 | 
			
		||||
modernc.org/libc v1.8.0/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
 | 
			
		||||
modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 | 
			
		||||
modernc.org/mathutil v1.2.2 h1:+yFk8hBprV+4c0U9GjFtL+dV3N8hOJ8JCituQcMShFY=
 | 
			
		||||
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 | 
			
		||||
modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM=
 | 
			
		||||
modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc=
 | 
			
		||||
modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A=
 | 
			
		||||
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
 | 
			
		||||
modernc.org/sqlite v1.10.1-0.20210314190707-798bbeb9bb84 h1:rgEUzE849tFlHSoeCrKyS9cZAljC+DY7MdMHKq6R6sY=
 | 
			
		||||
modernc.org/sqlite v1.10.1-0.20210314190707-798bbeb9bb84/go.mod h1:PGzq6qlhyYjL6uVbSgS6WoF7ZopTW/sI7+7p+mb4ZVU=
 | 
			
		||||
modernc.org/strutil v1.1.0 h1:+1/yCzZxY2pZwwrsbH+4T7BQMoLQ9QiBshRC9eicYsc=
 | 
			
		||||
modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
 | 
			
		||||
modernc.org/tcl v1.5.0 h1:euZSUNfE0Fd4W8VqXI1Ly1v7fqDJoBuAV88Ea+SnaSs=
 | 
			
		||||
modernc.org/tcl v1.5.0/go.mod h1:gb57hj4pO8fRrK54zveIfFXBaMHK3SKJNWcmRw1cRzc=
 | 
			
		||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
 | 
			
		||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 | 
			
		||||
modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
 | 
			
		||||
modernc.org/z v1.0.1 h1:WyIDpEpAIx4Hel6q/Pcgj/VhaQV5XPJ2I6ryIYbjnpc=
 | 
			
		||||
modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA=
 | 
			
		||||
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
 | 
			
		||||
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
 | 
			
		||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 | 
			
		||||
@@ -1676,8 +1706,9 @@ sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1
 | 
			
		||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs=
 | 
			
		||||
strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY=
 | 
			
		||||
xorm.io/builder v0.3.7/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
 | 
			
		||||
xorm.io/builder v0.3.8/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
 | 
			
		||||
xorm.io/builder v0.3.9 h1:Sd65/LdWyO7LR8+Cbd+e7mm3sK/7U9k0jS3999IDHMc=
 | 
			
		||||
xorm.io/builder v0.3.9/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
 | 
			
		||||
xorm.io/xorm v1.0.6/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
 | 
			
		||||
xorm.io/xorm v1.0.7 h1:26yBTDVI+CfQpVz2Y88fISh+aiJXIPP4eNoTJlwzsC4=
 | 
			
		||||
xorm.io/xorm v1.0.7/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
 | 
			
		||||
xorm.io/xorm v1.1.0 h1:mkEsQXLauZajiOld2cB2PkFcUZKePepPgs1bC1dw8RA=
 | 
			
		||||
xorm.io/xorm v1.1.0/go.mod h1:EDzNHMuCVZNszkIRSLL2nI0zX+nQE8RstAVranlSfqI=
 | 
			
		||||
 
 | 
			
		||||
@@ -130,11 +130,14 @@ func getNewRepoEditOption(opts *api.EditRepoOption) *api.EditRepoOption {
 | 
			
		||||
 | 
			
		||||
func TestAPIRepoEdit(t *testing.T) {
 | 
			
		||||
	onGiteaRun(t, func(t *testing.T, u *url.URL) {
 | 
			
		||||
		bFalse, bTrue := false, true
 | 
			
		||||
 | 
			
		||||
		user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)               // owner of the repo1 & repo16
 | 
			
		||||
		user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User)               // owner of the repo3, is an org
 | 
			
		||||
		user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User)               // owner of neither repos
 | 
			
		||||
		repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)   // public repo
 | 
			
		||||
		repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)   // public repo
 | 
			
		||||
		repo15 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 15}).(*models.Repository) // empty repo
 | 
			
		||||
		repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
 | 
			
		||||
 | 
			
		||||
		// Get user2's token
 | 
			
		||||
@@ -286,9 +289,8 @@ func TestAPIRepoEdit(t *testing.T) {
 | 
			
		||||
		// Test making a repo public that is private
 | 
			
		||||
		repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository)
 | 
			
		||||
		assert.True(t, repo16.IsPrivate)
 | 
			
		||||
		private := false
 | 
			
		||||
		repoEditOption = &api.EditRepoOption{
 | 
			
		||||
			Private: &private,
 | 
			
		||||
			Private: &bFalse,
 | 
			
		||||
		}
 | 
			
		||||
		url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo16.Name, token2)
 | 
			
		||||
		req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
 | 
			
		||||
@@ -296,11 +298,24 @@ func TestAPIRepoEdit(t *testing.T) {
 | 
			
		||||
		repo16 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository)
 | 
			
		||||
		assert.False(t, repo16.IsPrivate)
 | 
			
		||||
		// Make it private again
 | 
			
		||||
		private = true
 | 
			
		||||
		repoEditOption.Private = &private
 | 
			
		||||
		repoEditOption.Private = &bTrue
 | 
			
		||||
		req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
 | 
			
		||||
		_ = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		// Test to change empty repo
 | 
			
		||||
		assert.False(t, repo15.IsArchived)
 | 
			
		||||
		url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, repo15.Name, token2)
 | 
			
		||||
		req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
 | 
			
		||||
			Archived: &bTrue,
 | 
			
		||||
		})
 | 
			
		||||
		_ = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
		repo15 = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 15}).(*models.Repository)
 | 
			
		||||
		assert.True(t, repo15.IsArchived)
 | 
			
		||||
		req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
 | 
			
		||||
			Archived: &bFalse,
 | 
			
		||||
		})
 | 
			
		||||
		_ = session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		// Test using org repo "user3/repo3" where user2 is a collaborator
 | 
			
		||||
		origRepoEditOption = getRepoEditOptionFromRepo(repo3)
 | 
			
		||||
		repoEditOption = getNewRepoEditOption(origRepoEditOption)
 | 
			
		||||
 
 | 
			
		||||
@@ -223,7 +223,7 @@ func TestAPIViewRepo(t *testing.T) {
 | 
			
		||||
	DecodeJSON(t, resp, &repo)
 | 
			
		||||
	assert.EqualValues(t, 1, repo.ID)
 | 
			
		||||
	assert.EqualValues(t, "repo1", repo.Name)
 | 
			
		||||
	assert.EqualValues(t, 2, repo.Releases)
 | 
			
		||||
	assert.EqualValues(t, 1, repo.Releases)
 | 
			
		||||
	assert.EqualValues(t, 1, repo.OpenIssues)
 | 
			
		||||
	assert.EqualValues(t, 3, repo.OpenPulls)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										69
									
								
								integrations/git_smart_http_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								integrations/git_smart_http_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,69 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package integrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGitSmartHTTP(t *testing.T) {
 | 
			
		||||
	onGiteaRun(t, testGitSmartHTTP)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testGitSmartHTTP(t *testing.T, u *url.URL) {
 | 
			
		||||
	var kases = []struct {
 | 
			
		||||
		p    string
 | 
			
		||||
		code int
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			p:    "user2/repo1/info/refs",
 | 
			
		||||
			code: 200,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			p:    "user2/repo1/HEAD",
 | 
			
		||||
			code: 200,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			p:    "user2/repo1/objects/info/alternates",
 | 
			
		||||
			code: 404,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			p:    "user2/repo1/objects/info/http-alternates",
 | 
			
		||||
			code: 404,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			p:    "user2/repo1/../../custom/conf/app.ini",
 | 
			
		||||
			code: 404,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			p:    "user2/repo1/objects/info/../../../../custom/conf/app.ini",
 | 
			
		||||
			code: 404,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			p:    `user2/repo1/objects/info/..\..\..\..\custom\conf\app.ini`,
 | 
			
		||||
			code: 400,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, kase := range kases {
 | 
			
		||||
		t.Run(kase.p, func(t *testing.T) {
 | 
			
		||||
			p := u.String() + kase.p
 | 
			
		||||
			req, err := http.NewRequest("GET", p, nil)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			req.SetBasicAuth("user2", userPassword)
 | 
			
		||||
			resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
			defer resp.Body.Close()
 | 
			
		||||
			assert.EqualValues(t, kase.code, resp.StatusCode)
 | 
			
		||||
			_, err = ioutil.ReadAll(resp.Body)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								integrations/goget_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								integrations/goget_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package integrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGoGet(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	req := NewRequest(t, "GET", "/blah/glah/plah?go-get=1")
 | 
			
		||||
	resp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	expected := fmt.Sprintf(`<!doctype html>
 | 
			
		||||
<html>
 | 
			
		||||
	<head>
 | 
			
		||||
		<meta name="go-import" content="%[1]s:%[2]s/blah/glah git %[3]sblah/glah.git">
 | 
			
		||||
		<meta name="go-source" content="%[1]s:%[2]s/blah/glah _ %[3]sblah/glah/src/branch/master{/dir} %[3]sblah/glah/src/branch/master{/dir}/{file}#L{line}">
 | 
			
		||||
	</head>
 | 
			
		||||
	<body>
 | 
			
		||||
		go get --insecure %[1]s:%[2]s/blah/glah
 | 
			
		||||
	</body>
 | 
			
		||||
</html>
 | 
			
		||||
`, setting.Domain, setting.HTTPPort, setting.AppURL)
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, expected, resp.Body.String())
 | 
			
		||||
}
 | 
			
		||||
@@ -10,9 +10,11 @@ import (
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
 | 
			
		||||
	"github.com/PuerkitoBio/goquery"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/unknwon/i18n"
 | 
			
		||||
)
 | 
			
		||||
@@ -83,7 +85,7 @@ func TestCreateRelease(t *testing.T) {
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false)
 | 
			
		||||
 | 
			
		||||
	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 2)
 | 
			
		||||
	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateReleasePreRelease(t *testing.T) {
 | 
			
		||||
@@ -92,7 +94,7 @@ func TestCreateReleasePreRelease(t *testing.T) {
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false)
 | 
			
		||||
 | 
			
		||||
	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 2)
 | 
			
		||||
	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateReleaseDraft(t *testing.T) {
 | 
			
		||||
@@ -101,7 +103,7 @@ func TestCreateReleaseDraft(t *testing.T) {
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true)
 | 
			
		||||
 | 
			
		||||
	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 2)
 | 
			
		||||
	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 3)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateReleasePaging(t *testing.T) {
 | 
			
		||||
@@ -127,3 +129,80 @@ func TestCreateReleasePaging(t *testing.T) {
 | 
			
		||||
	session2 := loginUser(t, "user4")
 | 
			
		||||
	checkLatestReleaseAndCount(t, session2, "/user2/repo1", "v0.0.11", i18n.Tr("en", "repo.release.stable"), 10)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestViewReleaseListNoLogin(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
 | 
			
		||||
 | 
			
		||||
	link := repo.Link() + "/releases"
 | 
			
		||||
 | 
			
		||||
	req := NewRequest(t, "GET", link)
 | 
			
		||||
	rsp := MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, rsp.Body)
 | 
			
		||||
	releases := htmlDoc.Find("#release-list li.ui.grid")
 | 
			
		||||
	assert.Equal(t, 1, releases.Length())
 | 
			
		||||
 | 
			
		||||
	links := make([]string, 0, 5)
 | 
			
		||||
	releases.Each(func(i int, s *goquery.Selection) {
 | 
			
		||||
		link, exist := s.Find(".release-list-title a").Attr("href")
 | 
			
		||||
		if !exist {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		links = append(links, link)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.EqualValues(t, []string{"/user2/repo1/releases/tag/v1.1"}, links)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestViewReleaseListLogin(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
 | 
			
		||||
 | 
			
		||||
	link := repo.Link() + "/releases"
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, "user1")
 | 
			
		||||
	req := NewRequest(t, "GET", link)
 | 
			
		||||
	rsp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, rsp.Body)
 | 
			
		||||
	releases := htmlDoc.Find("#release-list li.ui.grid")
 | 
			
		||||
	assert.Equal(t, 2, releases.Length())
 | 
			
		||||
 | 
			
		||||
	links := make([]string, 0, 5)
 | 
			
		||||
	releases.Each(func(i int, s *goquery.Selection) {
 | 
			
		||||
		link, exist := s.Find(".release-list-title a").Attr("href")
 | 
			
		||||
		if !exist {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		links = append(links, link)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.EqualValues(t, []string{"/user2/repo1/releases/tag/draft-release",
 | 
			
		||||
		"/user2/repo1/releases/tag/v1.1"}, links)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestViewTagsList(t *testing.T) {
 | 
			
		||||
	defer prepareTestEnv(t)()
 | 
			
		||||
 | 
			
		||||
	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
 | 
			
		||||
 | 
			
		||||
	link := repo.Link() + "/tags"
 | 
			
		||||
 | 
			
		||||
	session := loginUser(t, "user1")
 | 
			
		||||
	req := NewRequest(t, "GET", link)
 | 
			
		||||
	rsp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
	htmlDoc := NewHTMLParser(t, rsp.Body)
 | 
			
		||||
	tags := htmlDoc.Find(".tag-list tr")
 | 
			
		||||
	assert.Equal(t, 2, tags.Length())
 | 
			
		||||
 | 
			
		||||
	tagNames := make([]string, 0, 5)
 | 
			
		||||
	tags.Each(func(i int, s *goquery.Selection) {
 | 
			
		||||
		tagNames = append(tagNames, s.Find(".tag a.df.ac").Text())
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	assert.EqualValues(t, []string{"delete-tag", "v1.1"}, tagNames)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -125,8 +125,8 @@ func getAttachmentByUUID(e Engine, uuid string) (*Attachment, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAttachmentsByUUIDs returns attachment by given UUID list.
 | 
			
		||||
func GetAttachmentsByUUIDs(uuids []string) ([]*Attachment, error) {
 | 
			
		||||
	return getAttachmentsByUUIDs(x, uuids)
 | 
			
		||||
func GetAttachmentsByUUIDs(ctx DBContext, uuids []string) ([]*Attachment, error) {
 | 
			
		||||
	return getAttachmentsByUUIDs(ctx.e, uuids)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getAttachmentsByUUIDs(e Engine, uuids []string) ([]*Attachment, error) {
 | 
			
		||||
@@ -183,12 +183,12 @@ func getAttachmentByReleaseIDFileName(e Engine, releaseID int64, fileName string
 | 
			
		||||
 | 
			
		||||
// DeleteAttachment deletes the given attachment and optionally the associated file.
 | 
			
		||||
func DeleteAttachment(a *Attachment, remove bool) error {
 | 
			
		||||
	_, err := DeleteAttachments([]*Attachment{a}, remove)
 | 
			
		||||
	_, err := DeleteAttachments(DefaultDBContext(), []*Attachment{a}, remove)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteAttachments deletes the given attachments and optionally the associated files.
 | 
			
		||||
func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
 | 
			
		||||
func DeleteAttachments(ctx DBContext, attachments []*Attachment, remove bool) (int, error) {
 | 
			
		||||
	if len(attachments) == 0 {
 | 
			
		||||
		return 0, nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -198,7 +198,7 @@ func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
 | 
			
		||||
		ids = append(ids, a.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cnt, err := x.In("id", ids).NoAutoCondition().Delete(attachments[0])
 | 
			
		||||
	cnt, err := ctx.e.In("id", ids).NoAutoCondition().Delete(attachments[0])
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -220,7 +220,7 @@ func DeleteAttachmentsByIssue(issueID int64, remove bool) (int, error) {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DeleteAttachments(attachments, remove)
 | 
			
		||||
	return DeleteAttachments(DefaultDBContext(), attachments, remove)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteAttachmentsByComment deletes all attachments associated with the given comment.
 | 
			
		||||
@@ -230,7 +230,7 @@ func DeleteAttachmentsByComment(commentID int64, remove bool) (int, error) {
 | 
			
		||||
		return 0, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return DeleteAttachments(attachments, remove)
 | 
			
		||||
	return DeleteAttachments(DefaultDBContext(), attachments, remove)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateAttachment updates the given attachment in database
 | 
			
		||||
@@ -238,6 +238,15 @@ func UpdateAttachment(atta *Attachment) error {
 | 
			
		||||
	return updateAttachment(x, atta)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateAttachmentByUUID Updates attachment via uuid
 | 
			
		||||
func UpdateAttachmentByUUID(ctx DBContext, attach *Attachment, cols ...string) error {
 | 
			
		||||
	if attach.UUID == "" {
 | 
			
		||||
		return fmt.Errorf("Attachement uuid should not blank")
 | 
			
		||||
	}
 | 
			
		||||
	_, err := ctx.e.Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateAttachment(e Engine, atta *Attachment) error {
 | 
			
		||||
	var sess *xorm.Session
 | 
			
		||||
	if atta.ID != 0 && atta.UUID == "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -120,7 +120,7 @@ func TestUpdateAttachment(t *testing.T) {
 | 
			
		||||
func TestGetAttachmentsByUUIDs(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	attachList, err := GetAttachmentsByUUIDs([]string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"})
 | 
			
		||||
	attachList, err := GetAttachmentsByUUIDs(DefaultDBContext(), []string{"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a17", "not-existing-uuid"})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, 2, len(attachList))
 | 
			
		||||
	assert.Equal(t, "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", attachList[0].UUID)
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ func LibravatarURL(email string) (*url.URL, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HashedAvatarLink returns an avatar link for a provided email
 | 
			
		||||
func HashedAvatarLink(email string) string {
 | 
			
		||||
func HashedAvatarLink(email string, size int) string {
 | 
			
		||||
	lowerEmail := strings.ToLower(strings.TrimSpace(email))
 | 
			
		||||
	sum := fmt.Sprintf("%x", md5.Sum([]byte(lowerEmail)))
 | 
			
		||||
	_, _ = cache.GetString("Avatar:"+sum, func() (string, error) {
 | 
			
		||||
@@ -108,6 +108,9 @@ func HashedAvatarLink(email string) string {
 | 
			
		||||
		}
 | 
			
		||||
		return lowerEmail, nil
 | 
			
		||||
	})
 | 
			
		||||
	if size > 0 {
 | 
			
		||||
		return setting.AppSubURL + "/avatar/" + url.PathEscape(sum) + "?size=" + strconv.Itoa(size)
 | 
			
		||||
	}
 | 
			
		||||
	return setting.AppSubURL + "/avatar/" + url.PathEscape(sum)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -129,7 +132,7 @@ func SizedAvatarLink(email string, size int) string {
 | 
			
		||||
		// This is the slow path that would need to call LibravatarURL() which
 | 
			
		||||
		// does DNS lookups. Avoid it by issuing a redirect so we don't block
 | 
			
		||||
		// the template render with network requests.
 | 
			
		||||
		return HashedAvatarLink(email)
 | 
			
		||||
		return HashedAvatarLink(email, size)
 | 
			
		||||
	} else if !setting.DisableGravatar {
 | 
			
		||||
		// copy GravatarSourceURL, because we will modify its Path.
 | 
			
		||||
		copyOfGravatarSourceURL := *setting.GravatarSourceURL
 | 
			
		||||
 
 | 
			
		||||
@@ -296,11 +296,15 @@ func CountOrphanedObjects(subject, refobject, joinCond string) (int64, error) {
 | 
			
		||||
 | 
			
		||||
// DeleteOrphanedObjects delete subjects with have no existing refobject anymore
 | 
			
		||||
func DeleteOrphanedObjects(subject, refobject, joinCond string) error {
 | 
			
		||||
	_, err := x.In("id", builder.Select("`"+subject+"`.id").
 | 
			
		||||
	subQuery := builder.Select("`"+subject+"`.id").
 | 
			
		||||
		From("`"+subject+"`").
 | 
			
		||||
		Join("LEFT", "`"+refobject+"`", joinCond).
 | 
			
		||||
		Where(builder.IsNull{"`" + refobject + "`.id"})).
 | 
			
		||||
		Delete("`" + subject + "`")
 | 
			
		||||
		Where(builder.IsNull{"`" + refobject + "`.id"})
 | 
			
		||||
	sql, args, err := builder.Delete(builder.In("id", subQuery)).From("`" + subject + "`").ToSQL()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = x.Exec(append([]interface{}{sql}, args...)...)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								models/consistency_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								models/consistency_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
// Copyright 2021 Gitea. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestDeleteOrphanedObjects(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	countBefore, err := x.Count(&PullRequest{})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	_, err = x.Insert(&PullRequest{IssueID: 1000}, &PullRequest{IssueID: 1001}, &PullRequest{IssueID: 1003})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	orphaned, err := CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, 3, orphaned)
 | 
			
		||||
 | 
			
		||||
	err = DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	countAfter, err := x.Count(&PullRequest{})
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.EqualValues(t, countBefore, countAfter)
 | 
			
		||||
}
 | 
			
		||||
@@ -43,3 +43,15 @@
 | 
			
		||||
  is_tag: true
 | 
			
		||||
  created_unix: 946684800
 | 
			
		||||
 | 
			
		||||
-
 | 
			
		||||
  id: 4
 | 
			
		||||
  repo_id: 1
 | 
			
		||||
  publisher_id: 2
 | 
			
		||||
  tag_name: "draft-release"
 | 
			
		||||
  lower_tag_name: "draft-release"
 | 
			
		||||
  target: "master"
 | 
			
		||||
  title: "draft-release"
 | 
			
		||||
  is_draft: true
 | 
			
		||||
  is_prerelease: false
 | 
			
		||||
  is_tag: false
 | 
			
		||||
  created_unix: 1619524806
 | 
			
		||||
 
 | 
			
		||||
@@ -1086,7 +1086,7 @@ func getIssuesByIDs(e Engine, issueIDs []int64) ([]*Issue, error) {
 | 
			
		||||
 | 
			
		||||
func getIssueIDsByRepoID(e Engine, repoID int64) ([]int64, error) {
 | 
			
		||||
	ids := make([]int64, 0, 10)
 | 
			
		||||
	err := e.Table("issue").Where("repo_id = ?", repoID).Find(&ids)
 | 
			
		||||
	err := e.Table("issue").Cols("id").Where("repo_id = ?", repoID).Find(&ids)
 | 
			
		||||
	return ids, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,14 @@ func TestIssue_ReplaceLabels(t *testing.T) {
 | 
			
		||||
	testSuccess(1, []int64{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_GetIssueIDsByRepoID(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	ids, err := GetIssueIDsByRepoID(1)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Len(t, ids, 5)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIssueAPIURL(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, PrepareTestDatabase())
 | 
			
		||||
	issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	gouuid "github.com/google/uuid"
 | 
			
		||||
	jsoniter "github.com/json-iterator/go"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/xorm"
 | 
			
		||||
@@ -116,6 +117,7 @@ func (cfg *SMTPConfig) ToDB() ([]byte, error) {
 | 
			
		||||
// PAMConfig holds configuration for the PAM login source.
 | 
			
		||||
type PAMConfig struct {
 | 
			
		||||
	ServiceName string // pam service (e.g. system-auth)
 | 
			
		||||
	EmailDomain string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FromDB fills up a PAMConfig from serialized format.
 | 
			
		||||
@@ -696,15 +698,26 @@ func LoginViaPAM(user *User, login, password string, sourceID int64, cfg *PAMCon
 | 
			
		||||
 | 
			
		||||
	// Allow PAM sources with `@` in their name, like from Active Directory
 | 
			
		||||
	username := pamLogin
 | 
			
		||||
	email := pamLogin
 | 
			
		||||
	idx := strings.Index(pamLogin, "@")
 | 
			
		||||
	if idx > -1 {
 | 
			
		||||
		username = pamLogin[:idx]
 | 
			
		||||
	}
 | 
			
		||||
	if ValidateEmail(email) != nil {
 | 
			
		||||
		if cfg.EmailDomain != "" {
 | 
			
		||||
			email = fmt.Sprintf("%s@%s", username, cfg.EmailDomain)
 | 
			
		||||
		} else {
 | 
			
		||||
			email = fmt.Sprintf("%s@%s", username, setting.Service.NoReplyAddress)
 | 
			
		||||
		}
 | 
			
		||||
		if ValidateEmail(email) != nil {
 | 
			
		||||
			email = gouuid.New().String() + "@localhost"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user = &User{
 | 
			
		||||
		LowerName:   strings.ToLower(username),
 | 
			
		||||
		Name:        username,
 | 
			
		||||
		Email:       pamLogin,
 | 
			
		||||
		Email:       email,
 | 
			
		||||
		Passwd:      password,
 | 
			
		||||
		LoginType:   LoginPAM,
 | 
			
		||||
		LoginSource: sourceID,
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,7 @@ func fixPublisherIDforTagReleases(x *xorm.Engine) error {
 | 
			
		||||
				repo = new(Repository)
 | 
			
		||||
				has, err := sess.ID(release.RepoID).Get(repo)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Error("Error whilst loading repository[%d] for release[%d] with tag name %s. Error: %v", release.RepoID, release.ID, release.TagName, err)
 | 
			
		||||
					return err
 | 
			
		||||
				} else if !has {
 | 
			
		||||
					log.Warn("Release[%d] is orphaned and refers to non-existing repository %d", release.ID, release.RepoID)
 | 
			
		||||
@@ -99,28 +100,55 @@ func fixPublisherIDforTagReleases(x *xorm.Engine) error {
 | 
			
		||||
					// v120.go migration may not have been run correctly - we'll just replicate it here
 | 
			
		||||
					// because this appears to be a common-ish problem.
 | 
			
		||||
					if _, err := sess.Exec("UPDATE repository SET owner_name = (SELECT name FROM `user` WHERE `user`.id = repository.owner_id)"); err != nil {
 | 
			
		||||
						log.Error("Error whilst updating repository[%d] owner name", repo.ID)
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if _, err := sess.ID(release.RepoID).Get(repo); err != nil {
 | 
			
		||||
						log.Error("Error whilst loading repository[%d] for release[%d] with tag name %s. Error: %v", release.RepoID, release.ID, release.TagName, err)
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				gitRepo, err = git.OpenRepository(repoPath(repo.OwnerName, repo.Name))
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Error("Error whilst opening git repo for [%d]%s/%s. Error: %v", repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			commit, err := gitRepo.GetTagCommit(release.TagName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				if git.IsErrNotExist(err) {
 | 
			
		||||
					log.Warn("Unable to find commit %s for Tag: %s in [%d]%s/%s. Cannot update publisher ID.", err.(git.ErrNotExist).ID, release.TagName, repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				log.Error("Error whilst getting commit for Tag: %s in [%d]%s/%s. Error: %v", release.TagName, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
				return fmt.Errorf("GetTagCommit: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if commit.Author.Email == "" {
 | 
			
		||||
				log.Warn("Tag: %s in Repo[%d]%s/%s does not have a tagger.", release.TagName, repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
				commit, err = gitRepo.GetCommit(commit.ID.String())
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					if git.IsErrNotExist(err) {
 | 
			
		||||
						log.Warn("Unable to find commit %s for Tag: %s in [%d]%s/%s. Cannot update publisher ID.", err.(git.ErrNotExist).ID, release.TagName, repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
					log.Error("Error whilst getting commit for Tag: %s in [%d]%s/%s. Error: %v", release.TagName, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
					return fmt.Errorf("GetCommit: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if commit.Author.Email == "" {
 | 
			
		||||
				log.Warn("Tag: %s in Repo[%d]%s/%s does not have a Tagger and its underlying commit does not have an Author either!", release.TagName, repo.ID, repo.OwnerName, repo.Name)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if user == nil || !strings.EqualFold(user.Email, commit.Author.Email) {
 | 
			
		||||
				user = new(User)
 | 
			
		||||
				_, err = sess.Where("email=?", commit.Author.Email).Get(user)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Error("Error whilst getting commit author by email: %s for Tag: %s in [%d]%s/%s. Error: %v", commit.Author.Email, release.TagName, repo.ID, repo.OwnerName, repo.Name, err)
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
@@ -133,6 +161,7 @@ func fixPublisherIDforTagReleases(x *xorm.Engine) error {
 | 
			
		||||
 | 
			
		||||
			release.PublisherID = user.ID
 | 
			
		||||
			if _, err := sess.ID(release.ID).Cols("publisher_id").Update(release); err != nil {
 | 
			
		||||
				log.Error("Error whilst updating publisher[%d] for release[%d] with tag name %s. Error: %v", release.PublisherID, release.ID, release.TagName, err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -319,7 +319,7 @@ func DumpDatabase(filePath, dbType string) error {
 | 
			
		||||
		ID      int64 `xorm:"pk autoincr"`
 | 
			
		||||
		Version int64
 | 
			
		||||
	}
 | 
			
		||||
	t, err := x.TableInfo(Version{})
 | 
			
		||||
	t, err := x.TableInfo(&Version{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ func TestDumpDatabase(t *testing.T) {
 | 
			
		||||
		ID      int64 `xorm:"pk autoincr"`
 | 
			
		||||
		Version int64
 | 
			
		||||
	}
 | 
			
		||||
	assert.NoError(t, x.Sync2(Version{}))
 | 
			
		||||
	assert.NoError(t, x.Sync2(new(Version)))
 | 
			
		||||
 | 
			
		||||
	for _, dbName := range setting.SupportedDatabases {
 | 
			
		||||
		dbType := setting.GetDBTypeByName(dbName)
 | 
			
		||||
 
 | 
			
		||||
@@ -212,12 +212,21 @@ func (pr *PullRequest) GetDefaultMergeMessage() string {
 | 
			
		||||
		log.Error("Cannot load issue %d for PR id %d: Error: %v", pr.IssueID, pr.ID, err)
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pr.BaseRepoID == pr.HeadRepoID {
 | 
			
		||||
		return fmt.Sprintf("Merge pull request '%s' (#%d) from %s into %s", pr.Issue.Title, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch)
 | 
			
		||||
	if err := pr.LoadBaseRepo(); err != nil {
 | 
			
		||||
		log.Error("LoadBaseRepo: %v", err)
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("Merge pull request '%s' (#%d) from %s:%s into %s", pr.Issue.Title, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch)
 | 
			
		||||
	issueReference := "#"
 | 
			
		||||
	if pr.BaseRepo.UnitEnabled(UnitTypeExternalTracker) {
 | 
			
		||||
		issueReference = "!"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pr.BaseRepoID == pr.HeadRepoID {
 | 
			
		||||
		return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReviewCount represents a count of Reviews
 | 
			
		||||
 
 | 
			
		||||
@@ -234,3 +234,36 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
 | 
			
		||||
	pr.Issue.Title = "[wip] " + original
 | 
			
		||||
	assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, PrepareTestDatabase())
 | 
			
		||||
	pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2}).(*PullRequest)
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", pr.GetDefaultMergeMessage())
 | 
			
		||||
 | 
			
		||||
	pr.BaseRepoID = 1
 | 
			
		||||
	pr.HeadRepoID = 2
 | 
			
		||||
	assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", pr.GetDefaultMergeMessage())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	externalTracker := RepoUnit{
 | 
			
		||||
		Type: UnitTypeExternalTracker,
 | 
			
		||||
		Config: &ExternalTrackerConfig{
 | 
			
		||||
			ExternalTrackerFormat: "https://someurl.com/{user}/{repo}/{issue}",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	baseRepo := &Repository{Name: "testRepo", ID: 1}
 | 
			
		||||
	baseRepo.Owner = &User{Name: "testOwner"}
 | 
			
		||||
	baseRepo.Units = []*RepoUnit{&externalTracker}
 | 
			
		||||
 | 
			
		||||
	pr := AssertExistsAndLoadBean(t, &PullRequest{ID: 2, BaseRepo: baseRepo}).(*PullRequest)
 | 
			
		||||
 | 
			
		||||
	assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", pr.GetDefaultMergeMessage())
 | 
			
		||||
 | 
			
		||||
	pr.BaseRepoID = 1
 | 
			
		||||
	pr.HeadRepoID = 2
 | 
			
		||||
	assert.Equal(t, "Merge pull request 'issue3' (!3) from user2/repo1:branch2 into master", pr.GetDefaultMergeMessage())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
package models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -117,17 +118,20 @@ func UpdateRelease(ctx DBContext, rel *Release) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AddReleaseAttachments adds a release attachments
 | 
			
		||||
func AddReleaseAttachments(releaseID int64, attachmentUUIDs []string) (err error) {
 | 
			
		||||
func AddReleaseAttachments(ctx DBContext, releaseID int64, attachmentUUIDs []string) (err error) {
 | 
			
		||||
	// Check attachments
 | 
			
		||||
	attachments, err := GetAttachmentsByUUIDs(attachmentUUIDs)
 | 
			
		||||
	attachments, err := getAttachmentsByUUIDs(ctx.e, attachmentUUIDs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %v", attachmentUUIDs, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range attachments {
 | 
			
		||||
		if attachments[i].ReleaseID != 0 {
 | 
			
		||||
			return errors.New("release permission denied")
 | 
			
		||||
		}
 | 
			
		||||
		attachments[i].ReleaseID = releaseID
 | 
			
		||||
		// No assign value could be 0, so ignore AllCols().
 | 
			
		||||
		if _, err = x.ID(attachments[i].ID).Update(attachments[i]); err != nil {
 | 
			
		||||
		if _, err = ctx.e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
 | 
			
		||||
			return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -749,7 +749,7 @@ func (repo *Repository) updateSize(e Engine) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repo.Size = size
 | 
			
		||||
	_, err = e.ID(repo.ID).Cols("size").Update(repo)
 | 
			
		||||
	_, err = e.ID(repo.ID).Cols("size").NoAutoTime().Update(repo)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -1349,6 +1349,26 @@ func UpdateRepository(repo *Repository, visibilityChanged bool) (err error) {
 | 
			
		||||
	return sess.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepositoryOwnerNames updates repository owner_names (this should only be used when the ownerName has changed case)
 | 
			
		||||
func UpdateRepositoryOwnerNames(ownerID int64, ownerName string) error {
 | 
			
		||||
	if ownerID == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	sess := x.NewSession()
 | 
			
		||||
	defer sess.Close()
 | 
			
		||||
	if err := sess.Begin(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := sess.Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{
 | 
			
		||||
		OwnerName: ownerName,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return sess.Commit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdateRepositoryUpdatedTime updates a repository's updated time
 | 
			
		||||
func UpdateRepositoryUpdatedTime(repoID int64, updateTime time.Time) error {
 | 
			
		||||
	_, err := x.Exec("UPDATE repository SET updated_unix = ? WHERE id = ?", updateTime.Unix(), repoID)
 | 
			
		||||
@@ -1454,23 +1474,26 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
 | 
			
		||||
	if err := deleteBeans(sess,
 | 
			
		||||
		&Access{RepoID: repo.ID},
 | 
			
		||||
		&Action{RepoID: repo.ID},
 | 
			
		||||
		&Watch{RepoID: repoID},
 | 
			
		||||
		&Star{RepoID: repoID},
 | 
			
		||||
		&Mirror{RepoID: repoID},
 | 
			
		||||
		&Milestone{RepoID: repoID},
 | 
			
		||||
		&Release{RepoID: repoID},
 | 
			
		||||
		&Collaboration{RepoID: repoID},
 | 
			
		||||
		&PullRequest{BaseRepoID: repoID},
 | 
			
		||||
		&RepoUnit{RepoID: repoID},
 | 
			
		||||
		&RepoRedirect{RedirectRepoID: repoID},
 | 
			
		||||
		&Webhook{RepoID: repoID},
 | 
			
		||||
		&HookTask{RepoID: repoID},
 | 
			
		||||
		&Notification{RepoID: repoID},
 | 
			
		||||
		&CommitStatus{RepoID: repoID},
 | 
			
		||||
		&RepoIndexerStatus{RepoID: repoID},
 | 
			
		||||
		&LanguageStat{RepoID: repoID},
 | 
			
		||||
		&Comment{RefRepoID: repoID},
 | 
			
		||||
		&CommitStatus{RepoID: repoID},
 | 
			
		||||
		&DeletedBranch{RepoID: repoID},
 | 
			
		||||
		&HookTask{RepoID: repoID},
 | 
			
		||||
		&LFSLock{RepoID: repoID},
 | 
			
		||||
		&LanguageStat{RepoID: repoID},
 | 
			
		||||
		&Milestone{RepoID: repoID},
 | 
			
		||||
		&Mirror{RepoID: repoID},
 | 
			
		||||
		&Notification{RepoID: repoID},
 | 
			
		||||
		&ProtectedBranch{RepoID: repoID},
 | 
			
		||||
		&PullRequest{BaseRepoID: repoID},
 | 
			
		||||
		&Release{RepoID: repoID},
 | 
			
		||||
		&RepoIndexerStatus{RepoID: repoID},
 | 
			
		||||
		&RepoRedirect{RedirectRepoID: repoID},
 | 
			
		||||
		&RepoUnit{RepoID: repoID},
 | 
			
		||||
		&Star{RepoID: repoID},
 | 
			
		||||
		&Task{RepoID: repoID},
 | 
			
		||||
		&Watch{RepoID: repoID},
 | 
			
		||||
		&Webhook{RepoID: repoID},
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		return fmt.Errorf("deleteBeans: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -1486,10 +1509,6 @@ func DeleteRepository(doer *User, uid, repoID int64) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := sess.Where("repo_id = ?", repoID).Delete(new(RepoUnit)); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if repo.IsFork {
 | 
			
		||||
		if _, err := sess.Exec("UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil {
 | 
			
		||||
			return fmt.Errorf("decrease fork count: %v", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -117,6 +117,6 @@ func CountSessions() (int64, error) {
 | 
			
		||||
 | 
			
		||||
// CleanupSessions cleans up expired sessions
 | 
			
		||||
func CleanupSessions(maxLifetime int64) error {
 | 
			
		||||
	_, err := x.Where("created_unix <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
 | 
			
		||||
	_, err := x.Where("expiry <= ?", timeutil.TimeStampNow().Add(-maxLifetime)).Delete(&Session{})
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,11 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	migration "code.gitea.io/gitea/modules/migrations/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/secret"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	jsoniter "github.com/json-iterator/go"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
@@ -110,6 +113,24 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// decrypt credentials
 | 
			
		||||
		if opts.CloneAddrEncrypted != "" {
 | 
			
		||||
			if opts.CloneAddr, err = secret.DecryptSecret(setting.SecretKey, opts.CloneAddrEncrypted); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if opts.AuthPasswordEncrypted != "" {
 | 
			
		||||
			if opts.AuthPassword, err = secret.DecryptSecret(setting.SecretKey, opts.AuthPasswordEncrypted); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if opts.AuthTokenEncrypted != "" {
 | 
			
		||||
			if opts.AuthToken, err = secret.DecryptSecret(setting.SecretKey, opts.AuthTokenEncrypted); err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return &opts, nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, fmt.Errorf("Task type is %s, not Migrate Repo", task.Type.Name())
 | 
			
		||||
@@ -205,12 +226,31 @@ func createTask(e Engine, task *Task) error {
 | 
			
		||||
func FinishMigrateTask(task *Task) error {
 | 
			
		||||
	task.Status = structs.TaskStatusFinished
 | 
			
		||||
	task.EndTime = timeutil.TimeStampNow()
 | 
			
		||||
 | 
			
		||||
	// delete credentials when we're done, they're a liability.
 | 
			
		||||
	conf, err := task.MigrateConfig()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	conf.AuthPassword = ""
 | 
			
		||||
	conf.AuthToken = ""
 | 
			
		||||
	conf.CloneAddr = util.SanitizeURLCredentials(conf.CloneAddr, true)
 | 
			
		||||
	conf.AuthPasswordEncrypted = ""
 | 
			
		||||
	conf.AuthTokenEncrypted = ""
 | 
			
		||||
	conf.CloneAddrEncrypted = ""
 | 
			
		||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
			
		||||
	confBytes, err := json.Marshal(conf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	task.PayloadContent = string(confBytes)
 | 
			
		||||
 | 
			
		||||
	sess := x.NewSession()
 | 
			
		||||
	defer sess.Close()
 | 
			
		||||
	if err := sess.Begin(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if _, err := sess.ID(task.ID).Cols("status", "end_time").Update(task); err != nil {
 | 
			
		||||
	if _, err := sess.ID(task.ID).Cols("status", "end_time", "payload_content").Update(task); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,9 +57,15 @@ func GetAccessTokenBySHA(token string) (*AccessToken, error) {
 | 
			
		||||
	if token == "" {
 | 
			
		||||
		return nil, ErrAccessTokenEmpty{}
 | 
			
		||||
	}
 | 
			
		||||
	if len(token) < 8 {
 | 
			
		||||
	// A token is defined as being SHA1 sum these are 40 hexadecimal bytes long
 | 
			
		||||
	if len(token) != 40 {
 | 
			
		||||
		return nil, ErrAccessTokenNotExist{token}
 | 
			
		||||
	}
 | 
			
		||||
	for _, x := range []byte(token) {
 | 
			
		||||
		if x < '0' || (x > '9' && x < 'a') || x > 'f' {
 | 
			
		||||
			return nil, ErrAccessTokenNotExist{token}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	var tokens []AccessToken
 | 
			
		||||
	lastEight := token[len(token)-8:]
 | 
			
		||||
	err := x.Table(&AccessToken{}).Where("token_last_eight = ?", lastEight).Find(&tokens)
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,9 @@ func (u *User) RealSizedAvatarLink(size int) string {
 | 
			
		||||
		if u.Avatar == "" {
 | 
			
		||||
			return DefaultAvatarLink()
 | 
			
		||||
		}
 | 
			
		||||
		if size > 0 {
 | 
			
		||||
			return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size)
 | 
			
		||||
		}
 | 
			
		||||
		return setting.AppSubURL + "/avatars/" + u.Avatar
 | 
			
		||||
	case setting.DisableGravatar, setting.OfflineMode:
 | 
			
		||||
		if u.Avatar == "" {
 | 
			
		||||
@@ -89,7 +92,9 @@ func (u *User) RealSizedAvatarLink(size int) string {
 | 
			
		||||
				log.Error("GenerateRandomAvatar: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if size > 0 {
 | 
			
		||||
			return setting.AppSubURL + "/avatars/" + u.Avatar + "?size=" + strconv.Itoa(size)
 | 
			
		||||
		}
 | 
			
		||||
		return setting.AppSubURL + "/avatars/" + u.Avatar
 | 
			
		||||
	}
 | 
			
		||||
	return SizedAvatarLink(u.AvatarEmail, size)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
	"unicode"
 | 
			
		||||
	"unicode/utf8"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -213,19 +214,19 @@ func EllipsisString(str string, length int) string {
 | 
			
		||||
	if length <= 3 {
 | 
			
		||||
		return "..."
 | 
			
		||||
	}
 | 
			
		||||
	if len(str) <= length {
 | 
			
		||||
	if utf8.RuneCountInString(str) <= length {
 | 
			
		||||
		return str
 | 
			
		||||
	}
 | 
			
		||||
	return str[:length-3] + "..."
 | 
			
		||||
	return string([]rune(str)[:length-3]) + "..."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TruncateString returns a truncated string with given limit,
 | 
			
		||||
// it returns input string if length is not reached limit.
 | 
			
		||||
func TruncateString(str string, limit int) string {
 | 
			
		||||
	if len(str) < limit {
 | 
			
		||||
	if utf8.RuneCountInString(str) < limit {
 | 
			
		||||
		return str
 | 
			
		||||
	}
 | 
			
		||||
	return str[:limit]
 | 
			
		||||
	return string([]rune(str)[:limit])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StringsToInt64s converts a slice of string to a slice of int64.
 | 
			
		||||
 
 | 
			
		||||
@@ -170,6 +170,10 @@ func TestEllipsisString(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, "fo...", EllipsisString("foobar", 5))
 | 
			
		||||
	assert.Equal(t, "foobar", EllipsisString("foobar", 6))
 | 
			
		||||
	assert.Equal(t, "foobar", EllipsisString("foobar", 10))
 | 
			
		||||
	assert.Equal(t, "测...", EllipsisString("测试文本一二三四", 4))
 | 
			
		||||
	assert.Equal(t, "测试...", EllipsisString("测试文本一二三四", 5))
 | 
			
		||||
	assert.Equal(t, "测试文...", EllipsisString("测试文本一二三四", 6))
 | 
			
		||||
	assert.Equal(t, "测试文本一二三四", EllipsisString("测试文本一二三四", 10))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTruncateString(t *testing.T) {
 | 
			
		||||
@@ -181,6 +185,10 @@ func TestTruncateString(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, "fooba", TruncateString("foobar", 5))
 | 
			
		||||
	assert.Equal(t, "foobar", TruncateString("foobar", 6))
 | 
			
		||||
	assert.Equal(t, "foobar", TruncateString("foobar", 7))
 | 
			
		||||
	assert.Equal(t, "测试文本", TruncateString("测试文本一二三四", 4))
 | 
			
		||||
	assert.Equal(t, "测试文本一", TruncateString("测试文本一二三四", 5))
 | 
			
		||||
	assert.Equal(t, "测试文本一二", TruncateString("测试文本一二三四", 6))
 | 
			
		||||
	assert.Equal(t, "测试文本一二三", TruncateString("测试文本一二三四", 7))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestStringsToInt64s(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
package context
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/url"
 | 
			
		||||
@@ -393,7 +394,7 @@ func RepoIDAssignment() func(ctx *Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RepoAssignment returns a middleware to handle repository assignment
 | 
			
		||||
func RepoAssignment(ctx *Context) {
 | 
			
		||||
func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
 | 
			
		||||
	var (
 | 
			
		||||
		owner *models.User
 | 
			
		||||
		err   error
 | 
			
		||||
@@ -529,12 +530,12 @@ func RepoAssignment(ctx *Context) {
 | 
			
		||||
	ctx.Repo.GitRepo = gitRepo
 | 
			
		||||
 | 
			
		||||
	// We opened it, we should close it
 | 
			
		||||
	defer func() {
 | 
			
		||||
	cancel = func() {
 | 
			
		||||
		// If it's been set to nil then assume someone else has closed it.
 | 
			
		||||
		if ctx.Repo.GitRepo != nil {
 | 
			
		||||
			ctx.Repo.GitRepo.Close()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Stop at this point when the repo is empty.
 | 
			
		||||
	if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
@@ -619,6 +620,7 @@ func RepoAssignment(ctx *Context) {
 | 
			
		||||
		ctx.Data["GoDocDirectory"] = prefix + "{/dir}"
 | 
			
		||||
		ctx.Data["GoDocFile"] = prefix + "{/dir}/{file}#L{line}"
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RepoRefType type of repo reference
 | 
			
		||||
@@ -643,7 +645,7 @@ const (
 | 
			
		||||
 | 
			
		||||
// RepoRef handles repository reference names when the ref name is not
 | 
			
		||||
// explicitly given
 | 
			
		||||
func RepoRef() func(*Context) {
 | 
			
		||||
func RepoRef() func(*Context) context.CancelFunc {
 | 
			
		||||
	// since no ref name is explicitly specified, ok to just use branch
 | 
			
		||||
	return RepoRefByType(RepoRefBranch)
 | 
			
		||||
}
 | 
			
		||||
@@ -722,8 +724,8 @@ func getRefName(ctx *Context, pathType RepoRefType) string {
 | 
			
		||||
 | 
			
		||||
// RepoRefByType handles repository reference name for a specific type
 | 
			
		||||
// of repository reference
 | 
			
		||||
func RepoRefByType(refType RepoRefType) func(*Context) {
 | 
			
		||||
	return func(ctx *Context) {
 | 
			
		||||
func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context) context.CancelFunc {
 | 
			
		||||
	return func(ctx *Context) (cancel context.CancelFunc) {
 | 
			
		||||
		// Empty repository does not have reference information.
 | 
			
		||||
		if ctx.Repo.Repository.IsEmpty {
 | 
			
		||||
			return
 | 
			
		||||
@@ -742,12 +744,12 @@ func RepoRefByType(refType RepoRefType) func(*Context) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			// We opened it, we should close it
 | 
			
		||||
			defer func() {
 | 
			
		||||
			cancel = func() {
 | 
			
		||||
				// If it's been set to nil then assume someone else has closed it.
 | 
			
		||||
				if ctx.Repo.GitRepo != nil {
 | 
			
		||||
					ctx.Repo.GitRepo.Close()
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Get default branch.
 | 
			
		||||
@@ -811,6 +813,9 @@ func RepoRefByType(refType RepoRefType) func(*Context) {
 | 
			
		||||
						util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), refName, ctx.Repo.Commit.ID.String(), 1))))
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if len(ignoreNotExistErr) > 0 && ignoreNotExistErr[0] {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				ctx.NotFound("RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
@@ -841,6 +846,7 @@ func RepoRefByType(refType RepoRefType) func(*Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ func (r *Response) Write(bs []byte) (int, error) {
 | 
			
		||||
		return size, err
 | 
			
		||||
	}
 | 
			
		||||
	if r.status == 0 {
 | 
			
		||||
		r.WriteHeader(200)
 | 
			
		||||
		r.status = http.StatusOK
 | 
			
		||||
	}
 | 
			
		||||
	return size, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,11 @@ func ToNotificationThread(n *models.Notification) *api.NotificationThread {
 | 
			
		||||
			if err == nil && comment != nil {
 | 
			
		||||
				result.Subject.LatestCommentURL = comment.APIURL()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			pr, _ := n.Issue.GetPullRequest()
 | 
			
		||||
			if pr != nil && pr.HasMerged {
 | 
			
		||||
				result.Subject.State = "merged"
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case models.NotificationSourceCommit:
 | 
			
		||||
		result.Subject = &api.NotificationSubject{
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,7 @@ func innerToRepo(repo *models.Repository, mode models.AccessMode, isParent bool)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	numReleases, _ := models.GetReleaseCountByRepoID(repo.ID, models.FindReleasesOptions{IncludeDrafts: false, IncludeTags: true})
 | 
			
		||||
	numReleases, _ := models.GetReleaseCountByRepoID(repo.ID, models.FindReleasesOptions{IncludeDrafts: false, IncludeTags: false})
 | 
			
		||||
 | 
			
		||||
	mirrorInterval := ""
 | 
			
		||||
	if repo.IsMirror {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,13 +23,13 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find labels without existing repo or org
 | 
			
		||||
	count, err := models.CountOrphanedLabels()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned labels")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned labels", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedLabels(); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned labels")
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned labels", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d labels without existing repository/organisation deleted", count)
 | 
			
		||||
@@ -41,13 +41,13 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find IssueLabels without existing label
 | 
			
		||||
	count, err = models.CountOrphanedIssueLabels()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned issue_labels")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned issue_labels", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedIssueLabels(); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned issue_labels")
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned issue_labels", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d issue_labels without existing label deleted", count)
 | 
			
		||||
@@ -59,13 +59,13 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find issues without existing repository
 | 
			
		||||
	count, err = models.CountOrphanedIssues()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned issues")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned issues", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedIssues(); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned issues")
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned issues", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d issues without existing repository deleted", count)
 | 
			
		||||
@@ -77,13 +77,13 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find pulls without existing issues
 | 
			
		||||
	count, err = models.CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id"); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects")
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d pull requests without existing issue deleted", count)
 | 
			
		||||
@@ -95,13 +95,13 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find tracked times without existing issues/pulls
 | 
			
		||||
	count, err = models.CountOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedObjects("tracked_time", "issue", "tracked_time.issue_id=issue.id"); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects")
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d tracked times without existing issue deleted", count)
 | 
			
		||||
@@ -113,14 +113,14 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find null archived repositories
 | 
			
		||||
	count, err = models.CountNullArchivedRepository()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting null archived repositories")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting null archived repositories", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			updatedCount, err := models.FixNullArchivedRepository()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst fixing null archived repositories")
 | 
			
		||||
				logger.Critical("Error: %v whilst fixing null archived repositories", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d repositories with null is_archived updated", updatedCount)
 | 
			
		||||
@@ -132,14 +132,14 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	// find label comments with empty labels
 | 
			
		||||
	count, err = models.CountCommentTypeLabelWithEmptyLabel()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting label comments with empty labels")
 | 
			
		||||
		logger.Critical("Error: %v whilst counting label comments with empty labels", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			updatedCount, err := models.FixCommentTypeLabelWithEmptyLabel()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst removing label comments with empty labels")
 | 
			
		||||
				logger.Critical("Error: %v whilst removing label comments with empty labels", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d label comments with empty labels removed", updatedCount)
 | 
			
		||||
@@ -191,13 +191,14 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
	if setting.Database.UsePostgreSQL {
 | 
			
		||||
		count, err = models.CountBadSequences()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Critical("Error: %v whilst checking sequence values")
 | 
			
		||||
			logger.Critical("Error: %v whilst checking sequence values", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if count > 0 {
 | 
			
		||||
			if autofix {
 | 
			
		||||
				err := models.FixBadSequences()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Critical("Error: %v whilst attempting to fix sequences")
 | 
			
		||||
					logger.Critical("Error: %v whilst attempting to fix sequences", err)
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				logger.Info("%d sequences updated", count)
 | 
			
		||||
@@ -207,6 +208,60 @@ func checkDBConsistency(logger log.Logger, autofix bool) error {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// find protected branches without existing repository
 | 
			
		||||
	count, err = models.CountOrphanedObjects("protected_branch", "repository", "protected_branch.repo_id=repository.id")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedObjects("protected_branch", "repository", "protected_branch.repo_id=repository.id"); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d protected branches without existing repository deleted", count)
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Warn("%d protected branches without existing repository", count)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// find deleted branches without existing repository
 | 
			
		||||
	count, err = models.CountOrphanedObjects("deleted_branch", "repository", "deleted_branch.repo_id=repository.id")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedObjects("deleted_branch", "repository", "deleted_branch.repo_id=repository.id"); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d deleted branches without existing repository deleted", count)
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Warn("%d deleted branches without existing repository", count)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// find LFS locks without existing repository
 | 
			
		||||
	count, err = models.CountOrphanedObjects("lfs_lock", "repository", "lfs_lock.repo_id=repository.id")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Critical("Error: %v whilst counting orphaned objects", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if count > 0 {
 | 
			
		||||
		if autofix {
 | 
			
		||||
			if err = models.DeleteOrphanedObjects("lfs_lock", "repository", "lfs_lock.repo_id=repository.id"); err != nil {
 | 
			
		||||
				logger.Critical("Error: %v whilst deleting orphaned objects", err)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			logger.Info("%d LFS locks without existing repository deleted", count)
 | 
			
		||||
		} else {
 | 
			
		||||
			logger.Warn("%d LFS locks without existing repository", count)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ func checkDBVersion(logger log.Logger, autofix bool) error {
 | 
			
		||||
 | 
			
		||||
		err = models.NewEngine(context.Background(), migrations.Migrate)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Critical("Error: %v during migration")
 | 
			
		||||
			logger.Critical("Error: %v during migration", err)
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
package emoji
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"sort"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
@@ -145,6 +146,8 @@ func (n *rememberSecondWriteWriter) Write(p []byte) (int, error) {
 | 
			
		||||
	if n.writecount == 2 {
 | 
			
		||||
		n.idx = n.pos
 | 
			
		||||
		n.end = n.pos + len(p)
 | 
			
		||||
		n.pos += len(p)
 | 
			
		||||
		return len(p), io.EOF
 | 
			
		||||
	}
 | 
			
		||||
	n.pos += len(p)
 | 
			
		||||
	return len(p), nil
 | 
			
		||||
@@ -155,6 +158,8 @@ func (n *rememberSecondWriteWriter) WriteString(s string) (int, error) {
 | 
			
		||||
	if n.writecount == 2 {
 | 
			
		||||
		n.idx = n.pos
 | 
			
		||||
		n.end = n.pos + len(s)
 | 
			
		||||
		n.pos += len(s)
 | 
			
		||||
		return len(s), io.EOF
 | 
			
		||||
	}
 | 
			
		||||
	n.pos += len(s)
 | 
			
		||||
	return len(s), nil
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,7 @@ type AuthenticationForm struct {
 | 
			
		||||
	TLS                           bool
 | 
			
		||||
	SkipVerify                    bool
 | 
			
		||||
	PAMServiceName                string
 | 
			
		||||
	PAMEmailDomain                string
 | 
			
		||||
	Oauth2Provider                string
 | 
			
		||||
	Oauth2Key                     string
 | 
			
		||||
	Oauth2Secret                  string
 | 
			
		||||
 
 | 
			
		||||
@@ -149,17 +149,18 @@ headerLoop:
 | 
			
		||||
// constant hextable to help quickly convert between 20byte and 40byte hashes
 | 
			
		||||
const hextable = "0123456789abcdef"
 | 
			
		||||
 | 
			
		||||
// to40ByteSHA converts a 20-byte SHA in a 40-byte slice into a 40-byte sha in place
 | 
			
		||||
// without allocations. This is at least 100x quicker that hex.EncodeToString
 | 
			
		||||
// NB This requires that sha is a 40-byte slice
 | 
			
		||||
func to40ByteSHA(sha []byte) []byte {
 | 
			
		||||
// To40ByteSHA converts a 20-byte SHA into a 40-byte sha. Input and output can be the
 | 
			
		||||
// same 40 byte slice to support in place conversion without allocations.
 | 
			
		||||
// This is at least 100x quicker that hex.EncodeToString
 | 
			
		||||
// NB This requires that out is a 40-byte slice
 | 
			
		||||
func To40ByteSHA(sha, out []byte) []byte {
 | 
			
		||||
	for i := 19; i >= 0; i-- {
 | 
			
		||||
		v := sha[i]
 | 
			
		||||
		vhi, vlo := v>>4, v&0x0f
 | 
			
		||||
		shi, slo := hextable[vhi], hextable[vlo]
 | 
			
		||||
		sha[i*2], sha[i*2+1] = shi, slo
 | 
			
		||||
		out[i*2], out[i*2+1] = shi, slo
 | 
			
		||||
	}
 | 
			
		||||
	return sha
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseTreeLineSkipMode reads an entry from a tree in a cat-file --batch stream
 | 
			
		||||
 
 | 
			
		||||
@@ -124,12 +124,18 @@ func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.
 | 
			
		||||
 | 
			
		||||
	cmd := exec.CommandContext(ctx, c.name, c.args...)
 | 
			
		||||
	if env == nil {
 | 
			
		||||
		cmd.Env = append(os.Environ(), fmt.Sprintf("LC_ALL=%s", DefaultLocale))
 | 
			
		||||
		cmd.Env = os.Environ()
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd.Env = env
 | 
			
		||||
		cmd.Env = append(cmd.Env, fmt.Sprintf("LC_ALL=%s", DefaultLocale))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmd.Env = append(
 | 
			
		||||
		cmd.Env,
 | 
			
		||||
		fmt.Sprintf("LC_ALL=%s", DefaultLocale),
 | 
			
		||||
		// avoid prompting for credentials interactively, supported since git v2.3
 | 
			
		||||
		"GIT_TERMINAL_PROMPT=0",
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// TODO: verify if this is still needed in golang 1.15
 | 
			
		||||
	if goVersionLessThan115 {
 | 
			
		||||
		cmd.Env = append(cmd.Env, "GODEBUG=asyncpreemptoff=1")
 | 
			
		||||
 
 | 
			
		||||
@@ -102,10 +102,13 @@ func (tes Entries) GetCommitsInfo(commit *Commit, treePath string, cache *LastCo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getLastCommitForPathsByCache(commitID, treePath string, paths []string, cache *LastCommitCache) (map[string]*Commit, []string, error) {
 | 
			
		||||
	wr, rd, cancel := CatFileBatch(cache.repo.Path)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	var unHitEntryPaths []string
 | 
			
		||||
	var results = make(map[string]*Commit)
 | 
			
		||||
	for _, p := range paths {
 | 
			
		||||
		lastCommit, err := cache.Get(commitID, path.Join(treePath, p))
 | 
			
		||||
		lastCommit, err := cache.Get(commitID, path.Join(treePath, p), wr, rd)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -300,7 +303,7 @@ revListLoop:
 | 
			
		||||
					commits[0] = string(commitID)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			treeID = to40ByteSHA(treeID)
 | 
			
		||||
			treeID = To40ByteSHA(treeID, treeID)
 | 
			
		||||
			_, err = batchStdinWriter.Write(treeID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,9 @@ import (
 | 
			
		||||
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
 | 
			
		||||
func CommitFromReader(gitRepo *Repository, sha SHA1, reader io.Reader) (*Commit, error) {
 | 
			
		||||
	commit := &Commit{
 | 
			
		||||
		ID: sha,
 | 
			
		||||
		ID:        sha,
 | 
			
		||||
		Author:    &Signature{},
 | 
			
		||||
		Committer: &Signature{},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	payloadSB := new(strings.Builder)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@
 | 
			
		||||
package git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"io"
 | 
			
		||||
	"path"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -34,7 +36,7 @@ func NewLastCommitCache(repoPath string, gitRepo *Repository, ttl func() int64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get get the last commit information by commit id and entry path
 | 
			
		||||
func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
 | 
			
		||||
func (c *LastCommitCache) Get(ref, entryPath string, wr *io.PipeWriter, rd *bufio.Reader) (interface{}, error) {
 | 
			
		||||
	v := c.cache.Get(c.getCacheKey(c.repoPath, ref, entryPath))
 | 
			
		||||
	if vs, ok := v.(string); ok {
 | 
			
		||||
		log("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, vs)
 | 
			
		||||
@@ -46,7 +48,10 @@ func (c *LastCommitCache) Get(ref, entryPath string) (interface{}, error) {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		commit, err := c.repo.getCommit(id)
 | 
			
		||||
		if _, err := wr.Write([]byte(vs + "\n")); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		commit, err := c.repo.getCommitFromBatchReader(rd, id)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ package git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetNote retrieves the git-notes data for a given commit.
 | 
			
		||||
@@ -49,7 +50,13 @@ func GetNote(repo *Repository, commitID string, note *Note) error {
 | 
			
		||||
	}
 | 
			
		||||
	note.Message = d
 | 
			
		||||
 | 
			
		||||
	lastCommits, err := GetLastCommitForPaths(notes, "", []string{path})
 | 
			
		||||
	treePath := ""
 | 
			
		||||
	if idx := strings.LastIndex(path, "/"); idx > -1 {
 | 
			
		||||
		treePath = path[:idx]
 | 
			
		||||
		path = path[idx+1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lastCommits, err := GetLastCommitForPaths(notes, treePath, []string{path})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,6 @@ func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
 | 
			
		||||
 | 
			
		||||
	basePath := repo.Path
 | 
			
		||||
 | 
			
		||||
	hashStr := hash.String()
 | 
			
		||||
 | 
			
		||||
	// Use rev-list to provide us with all commits in order
 | 
			
		||||
	revListReader, revListWriter := io.Pipe()
 | 
			
		||||
	defer func() {
 | 
			
		||||
@@ -74,7 +72,7 @@ func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
 | 
			
		||||
 | 
			
		||||
	fnameBuf := make([]byte, 4096)
 | 
			
		||||
	modeBuf := make([]byte, 40)
 | 
			
		||||
	workingShaBuf := make([]byte, 40)
 | 
			
		||||
	workingShaBuf := make([]byte, 20)
 | 
			
		||||
 | 
			
		||||
	for scan.Scan() {
 | 
			
		||||
		// Get the next commit ID
 | 
			
		||||
@@ -127,12 +125,12 @@ func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
 | 
			
		||||
			case "tree":
 | 
			
		||||
				var n int64
 | 
			
		||||
				for n < size {
 | 
			
		||||
					mode, fname, sha, count, err := git.ParseTreeLine(batchReader, modeBuf, fnameBuf, workingShaBuf)
 | 
			
		||||
					mode, fname, sha20byte, count, err := git.ParseTreeLine(batchReader, modeBuf, fnameBuf, workingShaBuf)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return nil, err
 | 
			
		||||
					}
 | 
			
		||||
					n += int64(count)
 | 
			
		||||
					if bytes.Equal(sha, []byte(hashStr)) {
 | 
			
		||||
					if bytes.Equal(sha20byte, hash[:]) {
 | 
			
		||||
						result := LFSResult{
 | 
			
		||||
							Name:         curPath + string(fname),
 | 
			
		||||
							SHA:          curCommit.ID.String(),
 | 
			
		||||
@@ -142,7 +140,9 @@ func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
 | 
			
		||||
						}
 | 
			
		||||
						resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result
 | 
			
		||||
					} else if string(mode) == git.EntryModeTree.String() {
 | 
			
		||||
						trees = append(trees, sha)
 | 
			
		||||
						sha40Byte := make([]byte, 40)
 | 
			
		||||
						git.To40ByteSHA(sha20byte, sha40Byte)
 | 
			
		||||
						trees = append(trees, sha40Byte)
 | 
			
		||||
						paths = append(paths, curPath+string(fname)+"/")
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,10 @@ package git
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -34,6 +35,18 @@ func (repo *Repository) ResolveReference(name string) (string, error) {
 | 
			
		||||
 | 
			
		||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
 | 
			
		||||
func (repo *Repository) GetRefCommitID(name string) (string, error) {
 | 
			
		||||
	if strings.HasPrefix(name, "refs/") {
 | 
			
		||||
		// We're gonna try just reading the ref file as this is likely to be quicker than other options
 | 
			
		||||
		fileInfo, err := os.Lstat(filepath.Join(repo.Path, name))
 | 
			
		||||
		if err == nil && fileInfo.Mode().IsRegular() && fileInfo.Size() == 41 {
 | 
			
		||||
			ref, err := ioutil.ReadFile(filepath.Join(repo.Path, name))
 | 
			
		||||
 | 
			
		||||
			if err == nil && SHAPattern.Match(ref[:40]) && ref[40] == '\n' {
 | 
			
		||||
				return string(ref[:40]), nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stdout, err := NewCommand("show-ref", "--verify", "--hash", name).RunInDir(repo.Path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if strings.Contains(err.Error(), "not a valid ref") {
 | 
			
		||||
@@ -69,6 +82,11 @@ func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	bufReader := bufio.NewReader(stdoutReader)
 | 
			
		||||
 | 
			
		||||
	return repo.getCommitFromBatchReader(bufReader, id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (repo *Repository) getCommitFromBatchReader(bufReader *bufio.Reader, id SHA1) (*Commit, error) {
 | 
			
		||||
	_, typ, size, err := ReadBatchLine(bufReader)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if errors.Is(err, io.EOF) {
 | 
			
		||||
@@ -106,7 +124,6 @@ func (repo *Repository) getCommit(id SHA1) (*Commit, error) {
 | 
			
		||||
	case "commit":
 | 
			
		||||
		return CommitFromReader(repo, id, io.LimitReader(bufReader, size))
 | 
			
		||||
	default:
 | 
			
		||||
		_ = stdoutReader.CloseWithError(fmt.Errorf("unknown typ: %s", typ))
 | 
			
		||||
		log("Unknown typ: %s", typ)
 | 
			
		||||
		return nil, ErrNotExist{
 | 
			
		||||
			ID: id.String(),
 | 
			
		||||
 
 | 
			
		||||
@@ -7,23 +7,18 @@ package git
 | 
			
		||||
import (
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetLatestCommitTime(t *testing.T) {
 | 
			
		||||
	lct, err := GetLatestCommitTime(".")
 | 
			
		||||
	bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
 | 
			
		||||
	lct, err := GetLatestCommitTime(bareRepo1Path)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	// Time is in the past
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	assert.True(t, lct.Unix() < now.Unix(), "%d not smaller than %d", lct, now)
 | 
			
		||||
	// Time is after Mon Oct 23 03:52:09 2017 +0300
 | 
			
		||||
	// Time is Sun Jul 21 22:43:13 2019 +0200
 | 
			
		||||
	// which is the time of commit
 | 
			
		||||
	// d47b98c44c9a6472e44ab80efe65235e11c6da2a
 | 
			
		||||
	refTime, err := time.Parse("Mon Jan 02 15:04:05 2006 -0700", "Mon Oct 23 03:52:09 2017 +0300")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.True(t, lct.Unix() > refTime.Unix(), "%d not greater than %d", lct, refTime)
 | 
			
		||||
	// feaf4ba6bc635fec442f46ddd4512416ec43c2c2 (refs/heads/master)
 | 
			
		||||
	assert.EqualValues(t, 1563741793, lct.Unix())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRepoIsEmpty(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ func (tag *Tag) Commit() (*Commit, error) {
 | 
			
		||||
// \n\n separate headers from message
 | 
			
		||||
func parseTagData(data []byte) (*Tag, error) {
 | 
			
		||||
	tag := new(Tag)
 | 
			
		||||
	tag.Tagger = &Signature{}
 | 
			
		||||
	// we now have the contents of the commit object. Let's investigate...
 | 
			
		||||
	nextline := 0
 | 
			
		||||
l:
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ package gitgraph
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
@@ -216,10 +217,10 @@ func newRefsFromRefNames(refNames []byte) []git.Reference {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		refName := string(refNameBytes)
 | 
			
		||||
		if refName[0:5] == "tag: " {
 | 
			
		||||
			refName = refName[5:]
 | 
			
		||||
		} else if refName[0:8] == "HEAD -> " {
 | 
			
		||||
			refName = refName[8:]
 | 
			
		||||
		if strings.HasPrefix(refName, "tag: ") {
 | 
			
		||||
			refName = strings.TrimPrefix(refName, "tag: ")
 | 
			
		||||
		} else if strings.HasPrefix(refName, "HEAD -> ") {
 | 
			
		||||
			refName = strings.TrimPrefix(refName, "HEAD -> ")
 | 
			
		||||
		}
 | 
			
		||||
		refs = append(refs, git.Reference{
 | 
			
		||||
			Name: refName,
 | 
			
		||||
 
 | 
			
		||||
@@ -74,12 +74,14 @@ func (g *Manager) start() {
 | 
			
		||||
 | 
			
		||||
	// Make SVC process
 | 
			
		||||
	run := svc.Run
 | 
			
		||||
	isWindowsService, err := svc.IsWindowsService()
 | 
			
		||||
 | 
			
		||||
	//lint:ignore SA1019 We use IsAnInteractiveSession because IsWindowsService has a different permissions profile
 | 
			
		||||
	isAnInteractiveSession, err := svc.IsAnInteractiveSession()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to ascertain if running as an Windows Service: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !isWindowsService {
 | 
			
		||||
	if isAnInteractiveSession {
 | 
			
		||||
		log.Trace("Not running a service ... using the debug SVC manager")
 | 
			
		||||
		run = debug.Run
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
@@ -26,6 +27,10 @@ var (
 | 
			
		||||
	DefaultWriteTimeOut time.Duration
 | 
			
		||||
	// DefaultMaxHeaderBytes default max header bytes
 | 
			
		||||
	DefaultMaxHeaderBytes int
 | 
			
		||||
	// PerWriteWriteTimeout timeout for writes
 | 
			
		||||
	PerWriteWriteTimeout = 30 * time.Second
 | 
			
		||||
	// PerWriteWriteTimeoutKbTime is a timeout taking account of how much there is to be written
 | 
			
		||||
	PerWriteWriteTimeoutKbTime = 10 * time.Second
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
@@ -37,14 +42,16 @@ type ServeFunction = func(net.Listener) error
 | 
			
		||||
 | 
			
		||||
// Server represents our graceful server
 | 
			
		||||
type Server struct {
 | 
			
		||||
	network     string
 | 
			
		||||
	address     string
 | 
			
		||||
	listener    net.Listener
 | 
			
		||||
	wg          sync.WaitGroup
 | 
			
		||||
	state       state
 | 
			
		||||
	lock        *sync.RWMutex
 | 
			
		||||
	BeforeBegin func(network, address string)
 | 
			
		||||
	OnShutdown  func()
 | 
			
		||||
	network              string
 | 
			
		||||
	address              string
 | 
			
		||||
	listener             net.Listener
 | 
			
		||||
	wg                   sync.WaitGroup
 | 
			
		||||
	state                state
 | 
			
		||||
	lock                 *sync.RWMutex
 | 
			
		||||
	BeforeBegin          func(network, address string)
 | 
			
		||||
	OnShutdown           func()
 | 
			
		||||
	PerWriteTimeout      time.Duration
 | 
			
		||||
	PerWritePerKbTimeout time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewServer creates a server on network at provided address
 | 
			
		||||
@@ -55,11 +62,13 @@ func NewServer(network, address, name string) *Server {
 | 
			
		||||
		log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid())
 | 
			
		||||
	}
 | 
			
		||||
	srv := &Server{
 | 
			
		||||
		wg:      sync.WaitGroup{},
 | 
			
		||||
		state:   stateInit,
 | 
			
		||||
		lock:    &sync.RWMutex{},
 | 
			
		||||
		network: network,
 | 
			
		||||
		address: address,
 | 
			
		||||
		wg:                   sync.WaitGroup{},
 | 
			
		||||
		state:                stateInit,
 | 
			
		||||
		lock:                 &sync.RWMutex{},
 | 
			
		||||
		network:              network,
 | 
			
		||||
		address:              address,
 | 
			
		||||
		PerWriteTimeout:      setting.PerWriteTimeout,
 | 
			
		||||
		PerWritePerKbTimeout: setting.PerWritePerKbTimeout,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	srv.BeforeBegin = func(network, addr string) {
 | 
			
		||||
@@ -221,9 +230,11 @@ func (wl *wrappedListener) Accept() (net.Conn, error) {
 | 
			
		||||
	closed := int32(0)
 | 
			
		||||
 | 
			
		||||
	c = wrappedConn{
 | 
			
		||||
		Conn:   c,
 | 
			
		||||
		server: wl.server,
 | 
			
		||||
		closed: &closed,
 | 
			
		||||
		Conn:                 c,
 | 
			
		||||
		server:               wl.server,
 | 
			
		||||
		closed:               &closed,
 | 
			
		||||
		perWriteTimeout:      wl.server.PerWriteTimeout,
 | 
			
		||||
		perWritePerKbTimeout: wl.server.PerWritePerKbTimeout,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wl.server.wg.Add(1)
 | 
			
		||||
@@ -246,8 +257,25 @@ func (wl *wrappedListener) File() (*os.File, error) {
 | 
			
		||||
 | 
			
		||||
type wrappedConn struct {
 | 
			
		||||
	net.Conn
 | 
			
		||||
	server *Server
 | 
			
		||||
	closed *int32
 | 
			
		||||
	server               *Server
 | 
			
		||||
	closed               *int32
 | 
			
		||||
	deadline             time.Time
 | 
			
		||||
	perWriteTimeout      time.Duration
 | 
			
		||||
	perWritePerKbTimeout time.Duration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w wrappedConn) Write(p []byte) (n int, err error) {
 | 
			
		||||
	if w.perWriteTimeout > 0 {
 | 
			
		||||
		minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout
 | 
			
		||||
		minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout)
 | 
			
		||||
 | 
			
		||||
		w.deadline = w.deadline.Add(minTimeout)
 | 
			
		||||
		if minDeadline.After(w.deadline) {
 | 
			
		||||
			w.deadline = minDeadline
 | 
			
		||||
		}
 | 
			
		||||
		_ = w.Conn.SetWriteDeadline(w.deadline)
 | 
			
		||||
	}
 | 
			
		||||
	return w.Conn.Write(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (w wrappedConn) Close() error {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
@@ -26,11 +27,13 @@ func GetCacheControl() string {
 | 
			
		||||
// generateETag generates an ETag based on size, filename and file modification time
 | 
			
		||||
func generateETag(fi os.FileInfo) string {
 | 
			
		||||
	etag := fmt.Sprint(fi.Size()) + fi.Name() + fi.ModTime().UTC().Format(http.TimeFormat)
 | 
			
		||||
	return base64.StdEncoding.EncodeToString([]byte(etag))
 | 
			
		||||
	return `"` + base64.StdEncoding.EncodeToString([]byte(etag)) + `"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleTimeCache handles time-based caching for a HTTP request
 | 
			
		||||
func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
			
		||||
	w.Header().Set("Cache-Control", GetCacheControl())
 | 
			
		||||
 | 
			
		||||
	ifModifiedSince := req.Header.Get("If-Modified-Since")
 | 
			
		||||
	if ifModifiedSince != "" {
 | 
			
		||||
		t, err := time.Parse(http.TimeFormat, ifModifiedSince)
 | 
			
		||||
@@ -40,20 +43,40 @@ func HandleTimeCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set("Cache-Control", GetCacheControl())
 | 
			
		||||
	w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleEtagCache handles ETag-based caching for a HTTP request
 | 
			
		||||
func HandleEtagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
			
		||||
// HandleFileETagCache handles ETag-based caching for a HTTP request
 | 
			
		||||
func HandleFileETagCache(req *http.Request, w http.ResponseWriter, fi os.FileInfo) (handled bool) {
 | 
			
		||||
	etag := generateETag(fi)
 | 
			
		||||
	if req.Header.Get("If-None-Match") == etag {
 | 
			
		||||
		w.WriteHeader(http.StatusNotModified)
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return HandleGenericETagCache(req, w, etag)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// HandleGenericETagCache handles ETag-based caching for a HTTP request.
 | 
			
		||||
// It returns true if the request was handled.
 | 
			
		||||
func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag string) (handled bool) {
 | 
			
		||||
	if len(etag) > 0 {
 | 
			
		||||
		w.Header().Set("Etag", etag)
 | 
			
		||||
		if checkIfNoneMatchIsValid(req, etag) {
 | 
			
		||||
			w.WriteHeader(http.StatusNotModified)
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	w.Header().Set("Cache-Control", GetCacheControl())
 | 
			
		||||
	w.Header().Set("ETag", etag)
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// checkIfNoneMatchIsValid tests if the header If-None-Match matches the ETag
 | 
			
		||||
func checkIfNoneMatchIsValid(req *http.Request, etag string) bool {
 | 
			
		||||
	ifNoneMatch := req.Header.Get("If-None-Match")
 | 
			
		||||
	if len(ifNoneMatch) > 0 {
 | 
			
		||||
		for _, item := range strings.Split(ifNoneMatch, ",") {
 | 
			
		||||
			item = strings.TrimSpace(item)
 | 
			
		||||
			if item == etag {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								modules/httpcache/httpcache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								modules/httpcache/httpcache_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
// Copyright 2021 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package httpcache
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type mockFileInfo struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m mockFileInfo) Name() string       { return "gitea.test" }
 | 
			
		||||
func (m mockFileInfo) Size() int64        { return int64(10) }
 | 
			
		||||
func (m mockFileInfo) Mode() os.FileMode  { return os.ModePerm }
 | 
			
		||||
func (m mockFileInfo) ModTime() time.Time { return time.Time{} }
 | 
			
		||||
func (m mockFileInfo) IsDir() bool        { return false }
 | 
			
		||||
func (m mockFileInfo) Sys() interface{}   { return nil }
 | 
			
		||||
 | 
			
		||||
func TestHandleFileETagCache(t *testing.T) {
 | 
			
		||||
	fi := mockFileInfo{}
 | 
			
		||||
	etag := `"MTBnaXRlYS50ZXN0TW9uLCAwMSBKYW4gMDAwMSAwMDowMDowMCBHTVQ="`
 | 
			
		||||
 | 
			
		||||
	t.Run("No_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		handled := HandleFileETagCache(req, w, fi)
 | 
			
		||||
 | 
			
		||||
		assert.False(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 2)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Cache-Control")
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Wrong_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		req.Header.Set("If-None-Match", `"wrong etag"`)
 | 
			
		||||
 | 
			
		||||
		handled := HandleFileETagCache(req, w, fi)
 | 
			
		||||
 | 
			
		||||
		assert.False(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 2)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Cache-Control")
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Correct_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		req.Header.Set("If-None-Match", etag)
 | 
			
		||||
 | 
			
		||||
		handled := HandleFileETagCache(req, w, fi)
 | 
			
		||||
 | 
			
		||||
		assert.True(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 1)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
		assert.Equal(t, http.StatusNotModified, w.Code)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestHandleGenericETagCache(t *testing.T) {
 | 
			
		||||
	etag := `"test"`
 | 
			
		||||
 | 
			
		||||
	t.Run("No_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		handled := HandleGenericETagCache(req, w, etag)
 | 
			
		||||
 | 
			
		||||
		assert.False(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 2)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Cache-Control")
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Wrong_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		req.Header.Set("If-None-Match", `"wrong etag"`)
 | 
			
		||||
 | 
			
		||||
		handled := HandleGenericETagCache(req, w, etag)
 | 
			
		||||
 | 
			
		||||
		assert.False(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 2)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Cache-Control")
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Correct_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		req.Header.Set("If-None-Match", etag)
 | 
			
		||||
 | 
			
		||||
		handled := HandleGenericETagCache(req, w, etag)
 | 
			
		||||
 | 
			
		||||
		assert.True(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 1)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
		assert.Equal(t, http.StatusNotModified, w.Code)
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Multiple_Wrong_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		req.Header.Set("If-None-Match", `"wrong etag", "wrong etag "`)
 | 
			
		||||
 | 
			
		||||
		handled := HandleGenericETagCache(req, w, etag)
 | 
			
		||||
 | 
			
		||||
		assert.False(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 2)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Cache-Control")
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Multiple_Correct_If-None-Match", func(t *testing.T) {
 | 
			
		||||
		req := &http.Request{Header: make(http.Header)}
 | 
			
		||||
		w := httptest.NewRecorder()
 | 
			
		||||
 | 
			
		||||
		req.Header.Set("If-None-Match", `"wrong etag", `+etag)
 | 
			
		||||
 | 
			
		||||
		handled := HandleGenericETagCache(req, w, etag)
 | 
			
		||||
 | 
			
		||||
		assert.True(t, handled)
 | 
			
		||||
		assert.Len(t, w.Header(), 1)
 | 
			
		||||
		assert.Contains(t, w.Header(), "Etag")
 | 
			
		||||
		assert.Equal(t, etag, w.Header().Get("Etag"))
 | 
			
		||||
		assert.Equal(t, http.StatusNotModified, w.Code)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -325,7 +325,7 @@ func (r *Request) getResponse() (*http.Response, error) {
 | 
			
		||||
		trans = &http.Transport{
 | 
			
		||||
			TLSClientConfig: r.setting.TLSClientConfig,
 | 
			
		||||
			Proxy:           proxy,
 | 
			
		||||
			Dial:            TimeoutDialer(r.setting.ConnectTimeout, r.setting.ReadWriteTimeout),
 | 
			
		||||
			Dial:            TimeoutDialer(r.setting.ConnectTimeout),
 | 
			
		||||
		}
 | 
			
		||||
	} else if t, ok := trans.(*http.Transport); ok {
 | 
			
		||||
		if t.TLSClientConfig == nil {
 | 
			
		||||
@@ -335,7 +335,7 @@ func (r *Request) getResponse() (*http.Response, error) {
 | 
			
		||||
			t.Proxy = r.setting.Proxy
 | 
			
		||||
		}
 | 
			
		||||
		if t.Dial == nil {
 | 
			
		||||
			t.Dial = TimeoutDialer(r.setting.ConnectTimeout, r.setting.ReadWriteTimeout)
 | 
			
		||||
			t.Dial = TimeoutDialer(r.setting.ConnectTimeout)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -352,6 +352,7 @@ func (r *Request) getResponse() (*http.Response, error) {
 | 
			
		||||
	client := &http.Client{
 | 
			
		||||
		Transport: trans,
 | 
			
		||||
		Jar:       jar,
 | 
			
		||||
		Timeout:   r.setting.ReadWriteTimeout,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(r.setting.UserAgent) > 0 && len(r.req.Header.Get("User-Agent")) == 0 {
 | 
			
		||||
@@ -457,12 +458,12 @@ func (r *Request) Response() (*http.Response, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TimeoutDialer returns functions of connection dialer with timeout settings for http.Transport Dial field.
 | 
			
		||||
func TimeoutDialer(cTimeout time.Duration, rwTimeout time.Duration) func(net, addr string) (c net.Conn, err error) {
 | 
			
		||||
func TimeoutDialer(cTimeout time.Duration) func(net, addr string) (c net.Conn, err error) {
 | 
			
		||||
	return func(netw, addr string) (net.Conn, error) {
 | 
			
		||||
		conn, err := net.DialTimeout(netw, addr, cTimeout)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		return conn, conn.SetDeadline(time.Now().Add(rwTimeout))
 | 
			
		||||
		return conn, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,11 @@ func (db *DBIndexer) Index(id int64) error {
 | 
			
		||||
	// Get latest commit for default branch
 | 
			
		||||
	commitID, err := gitRepo.GetBranchCommitID(repo.DefaultBranch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to get commit ID for defaultbranch %s in %s", repo.DefaultBranch, repo.RepoPath())
 | 
			
		||||
		if git.IsErrBranchNotExist(err) || git.IsErrNotExist((err)) {
 | 
			
		||||
			log.Debug("Unable to get commit ID for defaultbranch %s in %s ... skipping this repository", repo.DefaultBranch, repo.RepoPath())
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		log.Error("Unable to get commit ID for defaultbranch %s in %s. Error: %v", repo.DefaultBranch, repo.RepoPath(), err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -87,6 +87,7 @@ func isLinkStr(link string) bool {
 | 
			
		||||
	return validLinksPattern.MatchString(link)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME: This function is not concurrent safe
 | 
			
		||||
func getIssueFullPattern() *regexp.Regexp {
 | 
			
		||||
	if issueFullPattern == nil {
 | 
			
		||||
		issueFullPattern = regexp.MustCompile(regexp.QuoteMeta(setting.AppURL) +
 | 
			
		||||
@@ -403,24 +404,19 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) {
 | 
			
		||||
		}
 | 
			
		||||
	case html.ElementNode:
 | 
			
		||||
		if node.Data == "img" {
 | 
			
		||||
			attrs := node.Attr
 | 
			
		||||
			for idx, attr := range attrs {
 | 
			
		||||
			for _, attr := range node.Attr {
 | 
			
		||||
				if attr.Key != "src" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				link := []byte(attr.Val)
 | 
			
		||||
				if len(link) > 0 && !IsLink(link) {
 | 
			
		||||
				if len(attr.Val) > 0 && !isLinkStr(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
 | 
			
		||||
					prefix := ctx.urlPrefix
 | 
			
		||||
					if ctx.isWikiMarkdown {
 | 
			
		||||
						prefix = util.URLJoin(prefix, "wiki", "raw")
 | 
			
		||||
					}
 | 
			
		||||
					prefix = strings.Replace(prefix, "/src/", "/media/", 1)
 | 
			
		||||
 | 
			
		||||
					lnk := string(link)
 | 
			
		||||
					lnk = util.URLJoin(prefix, lnk)
 | 
			
		||||
					link = []byte(lnk)
 | 
			
		||||
					attr.Val = util.URLJoin(prefix, attr.Val)
 | 
			
		||||
				}
 | 
			
		||||
				node.Attr[idx].Val = string(link)
 | 
			
		||||
			}
 | 
			
		||||
		} else if node.Data == "a" {
 | 
			
		||||
			visitText = false
 | 
			
		||||
@@ -610,26 +606,38 @@ func replaceContentList(node *html.Node, i, j int, newNodes []*html.Node) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func mentionProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	// We replace only the first mention; other mentions will be addressed later
 | 
			
		||||
	found, loc := references.FindFirstMentionBytes([]byte(node.Data))
 | 
			
		||||
	if !found {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	mention := node.Data[loc.Start:loc.End]
 | 
			
		||||
	var teams string
 | 
			
		||||
	teams, ok := ctx.metas["teams"]
 | 
			
		||||
	// FIXME: util.URLJoin may not be necessary here:
 | 
			
		||||
	// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 | 
			
		||||
	// is an AppSubURL link we can probably fallback to concatenation.
 | 
			
		||||
	// team mention should follow @orgName/teamName style
 | 
			
		||||
	if ok && strings.Contains(mention, "/") {
 | 
			
		||||
		mentionOrgAndTeam := strings.Split(mention, "/")
 | 
			
		||||
		if mentionOrgAndTeam[0][1:] == ctx.metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
 | 
			
		||||
			replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
 | 
			
		||||
	start := 0
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next && start < len(node.Data) {
 | 
			
		||||
		// We replace only the first mention; other mentions will be addressed later
 | 
			
		||||
		found, loc := references.FindFirstMentionBytes([]byte(node.Data[start:]))
 | 
			
		||||
		if !found {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
		loc.Start += start
 | 
			
		||||
		loc.End += start
 | 
			
		||||
		mention := node.Data[loc.Start:loc.End]
 | 
			
		||||
		var teams string
 | 
			
		||||
		teams, ok := ctx.metas["teams"]
 | 
			
		||||
		// FIXME: util.URLJoin may not be necessary here:
 | 
			
		||||
		// - setting.AppURL is defined to have a terminal '/' so unless mention[1:]
 | 
			
		||||
		// is an AppSubURL link we can probably fallback to concatenation.
 | 
			
		||||
		// team mention should follow @orgName/teamName style
 | 
			
		||||
		if ok && strings.Contains(mention, "/") {
 | 
			
		||||
			mentionOrgAndTeam := strings.Split(mention, "/")
 | 
			
		||||
			if mentionOrgAndTeam[0][1:] == ctx.metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") {
 | 
			
		||||
				replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, "org", ctx.metas["org"], "teams", mentionOrgAndTeam[1]), mention, "mention"))
 | 
			
		||||
				node = node.NextSibling.NextSibling
 | 
			
		||||
				start = 0
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			start = loc.End
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
		start = 0
 | 
			
		||||
	}
 | 
			
		||||
	replaceContent(node, loc.Start, loc.End, createLink(util.URLJoin(setting.AppURL, mention[1:]), mention, "mention"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
@@ -637,188 +645,195 @@ func shortLinkProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shortLinkProcessorFull(ctx *postProcessCtx, node *html.Node, noLink bool) {
 | 
			
		||||
	m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		m := shortLinkPattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	content := node.Data[m[2]:m[3]]
 | 
			
		||||
	tail := node.Data[m[4]:m[5]]
 | 
			
		||||
	props := make(map[string]string)
 | 
			
		||||
		content := node.Data[m[2]:m[3]]
 | 
			
		||||
		tail := node.Data[m[4]:m[5]]
 | 
			
		||||
		props := make(map[string]string)
 | 
			
		||||
 | 
			
		||||
	// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
 | 
			
		||||
	// It makes page handling terrible, but we prefer GitHub syntax
 | 
			
		||||
	// And fall back to MediaWiki only when it is obvious from the look
 | 
			
		||||
	// Of text and link contents
 | 
			
		||||
	sl := strings.Split(content, "|")
 | 
			
		||||
	for _, v := range sl {
 | 
			
		||||
		if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
 | 
			
		||||
			// There is no equal in this argument; this is a mandatory arg
 | 
			
		||||
			if props["name"] == "" {
 | 
			
		||||
				if isLinkStr(v) {
 | 
			
		||||
					// If we clearly see it is a link, we save it so
 | 
			
		||||
		// MediaWiki uses [[link|text]], while GitHub uses [[text|link]]
 | 
			
		||||
		// It makes page handling terrible, but we prefer GitHub syntax
 | 
			
		||||
		// And fall back to MediaWiki only when it is obvious from the look
 | 
			
		||||
		// Of text and link contents
 | 
			
		||||
		sl := strings.Split(content, "|")
 | 
			
		||||
		for _, v := range sl {
 | 
			
		||||
			if equalPos := strings.IndexByte(v, '='); equalPos == -1 {
 | 
			
		||||
				// There is no equal in this argument; this is a mandatory arg
 | 
			
		||||
				if props["name"] == "" {
 | 
			
		||||
					if isLinkStr(v) {
 | 
			
		||||
						// If we clearly see it is a link, we save it so
 | 
			
		||||
 | 
			
		||||
					// But first we need to ensure, that if both mandatory args provided
 | 
			
		||||
					// look like links, we stick to GitHub syntax
 | 
			
		||||
					if props["link"] != "" {
 | 
			
		||||
						props["name"] = props["link"]
 | 
			
		||||
						// But first we need to ensure, that if both mandatory args provided
 | 
			
		||||
						// look like links, we stick to GitHub syntax
 | 
			
		||||
						if props["link"] != "" {
 | 
			
		||||
							props["name"] = props["link"]
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						props["link"] = strings.TrimSpace(v)
 | 
			
		||||
					} else {
 | 
			
		||||
						props["name"] = v
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					props["link"] = strings.TrimSpace(v)
 | 
			
		||||
				} else {
 | 
			
		||||
					props["name"] = v
 | 
			
		||||
					props["link"] = strings.TrimSpace(v)
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				props["link"] = strings.TrimSpace(v)
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// There is an equal; optional argument.
 | 
			
		||||
				// There is an equal; optional argument.
 | 
			
		||||
 | 
			
		||||
			sep := strings.IndexByte(v, '=')
 | 
			
		||||
			key, val := v[:sep], html.UnescapeString(v[sep+1:])
 | 
			
		||||
				sep := strings.IndexByte(v, '=')
 | 
			
		||||
				key, val := v[:sep], html.UnescapeString(v[sep+1:])
 | 
			
		||||
 | 
			
		||||
			// When parsing HTML, x/net/html will change all quotes which are
 | 
			
		||||
			// not used for syntax into UTF-8 quotes. So checking val[0] won't
 | 
			
		||||
			// be enough, since that only checks a single byte.
 | 
			
		||||
			if len(val) > 1 {
 | 
			
		||||
				if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) ||
 | 
			
		||||
					(strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) {
 | 
			
		||||
					const lenQuote = len("‘")
 | 
			
		||||
					val = val[lenQuote : len(val)-lenQuote]
 | 
			
		||||
				} else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) ||
 | 
			
		||||
					(strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) {
 | 
			
		||||
					val = val[1 : len(val)-1]
 | 
			
		||||
				} else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") {
 | 
			
		||||
					const lenQuote = len("‘")
 | 
			
		||||
					val = val[1 : len(val)-lenQuote]
 | 
			
		||||
				// When parsing HTML, x/net/html will change all quotes which are
 | 
			
		||||
				// not used for syntax into UTF-8 quotes. So checking val[0] won't
 | 
			
		||||
				// be enough, since that only checks a single byte.
 | 
			
		||||
				if len(val) > 1 {
 | 
			
		||||
					if (strings.HasPrefix(val, "“") && strings.HasSuffix(val, "”")) ||
 | 
			
		||||
						(strings.HasPrefix(val, "‘") && strings.HasSuffix(val, "’")) {
 | 
			
		||||
						const lenQuote = len("‘")
 | 
			
		||||
						val = val[lenQuote : len(val)-lenQuote]
 | 
			
		||||
					} else if (strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"")) ||
 | 
			
		||||
						(strings.HasPrefix(val, "'") && strings.HasSuffix(val, "'")) {
 | 
			
		||||
						val = val[1 : len(val)-1]
 | 
			
		||||
					} else if strings.HasPrefix(val, "'") && strings.HasSuffix(val, "’") {
 | 
			
		||||
						const lenQuote = len("‘")
 | 
			
		||||
						val = val[1 : len(val)-lenQuote]
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				props[key] = val
 | 
			
		||||
			}
 | 
			
		||||
			props[key] = val
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var name, link string
 | 
			
		||||
	if props["link"] != "" {
 | 
			
		||||
		link = props["link"]
 | 
			
		||||
	} else if props["name"] != "" {
 | 
			
		||||
		link = props["name"]
 | 
			
		||||
	}
 | 
			
		||||
	if props["title"] != "" {
 | 
			
		||||
		name = props["title"]
 | 
			
		||||
	} else if props["name"] != "" {
 | 
			
		||||
		name = props["name"]
 | 
			
		||||
	} else {
 | 
			
		||||
		name = link
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	name += tail
 | 
			
		||||
	image := false
 | 
			
		||||
	switch ext := filepath.Ext(link); ext {
 | 
			
		||||
	// fast path: empty string, ignore
 | 
			
		||||
	case "":
 | 
			
		||||
		break
 | 
			
		||||
	case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
 | 
			
		||||
		image = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	childNode := &html.Node{}
 | 
			
		||||
	linkNode := &html.Node{
 | 
			
		||||
		FirstChild: childNode,
 | 
			
		||||
		LastChild:  childNode,
 | 
			
		||||
		Type:       html.ElementNode,
 | 
			
		||||
		Data:       "a",
 | 
			
		||||
		DataAtom:   atom.A,
 | 
			
		||||
	}
 | 
			
		||||
	childNode.Parent = linkNode
 | 
			
		||||
	absoluteLink := isLinkStr(link)
 | 
			
		||||
	if !absoluteLink {
 | 
			
		||||
		if image {
 | 
			
		||||
			link = strings.ReplaceAll(link, " ", "+")
 | 
			
		||||
		var name, link string
 | 
			
		||||
		if props["link"] != "" {
 | 
			
		||||
			link = props["link"]
 | 
			
		||||
		} else if props["name"] != "" {
 | 
			
		||||
			link = props["name"]
 | 
			
		||||
		}
 | 
			
		||||
		if props["title"] != "" {
 | 
			
		||||
			name = props["title"]
 | 
			
		||||
		} else if props["name"] != "" {
 | 
			
		||||
			name = props["name"]
 | 
			
		||||
		} else {
 | 
			
		||||
			link = strings.ReplaceAll(link, " ", "-")
 | 
			
		||||
		}
 | 
			
		||||
		if !strings.Contains(link, "/") {
 | 
			
		||||
			link = url.PathEscape(link)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	urlPrefix := ctx.urlPrefix
 | 
			
		||||
	if image {
 | 
			
		||||
		if !absoluteLink {
 | 
			
		||||
			if IsSameDomain(urlPrefix) {
 | 
			
		||||
				urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
 | 
			
		||||
			}
 | 
			
		||||
			if ctx.isWikiMarkdown {
 | 
			
		||||
				link = util.URLJoin("wiki", "raw", link)
 | 
			
		||||
			}
 | 
			
		||||
			link = util.URLJoin(urlPrefix, link)
 | 
			
		||||
		}
 | 
			
		||||
		title := props["title"]
 | 
			
		||||
		if title == "" {
 | 
			
		||||
			title = props["alt"]
 | 
			
		||||
		}
 | 
			
		||||
		if title == "" {
 | 
			
		||||
			title = path.Base(name)
 | 
			
		||||
		}
 | 
			
		||||
		alt := props["alt"]
 | 
			
		||||
		if alt == "" {
 | 
			
		||||
			alt = name
 | 
			
		||||
			name = link
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// make the childNode an image - if we can, we also place the alt
 | 
			
		||||
		childNode.Type = html.ElementNode
 | 
			
		||||
		childNode.Data = "img"
 | 
			
		||||
		childNode.DataAtom = atom.Img
 | 
			
		||||
		childNode.Attr = []html.Attribute{
 | 
			
		||||
			{Key: "src", Val: link},
 | 
			
		||||
			{Key: "title", Val: title},
 | 
			
		||||
			{Key: "alt", Val: alt},
 | 
			
		||||
		name += tail
 | 
			
		||||
		image := false
 | 
			
		||||
		switch ext := filepath.Ext(link); ext {
 | 
			
		||||
		// fast path: empty string, ignore
 | 
			
		||||
		case "":
 | 
			
		||||
			// leave image as false
 | 
			
		||||
		case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg":
 | 
			
		||||
			image = true
 | 
			
		||||
		}
 | 
			
		||||
		if alt == "" {
 | 
			
		||||
			childNode.Attr = childNode.Attr[:2]
 | 
			
		||||
 | 
			
		||||
		childNode := &html.Node{}
 | 
			
		||||
		linkNode := &html.Node{
 | 
			
		||||
			FirstChild: childNode,
 | 
			
		||||
			LastChild:  childNode,
 | 
			
		||||
			Type:       html.ElementNode,
 | 
			
		||||
			Data:       "a",
 | 
			
		||||
			DataAtom:   atom.A,
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		childNode.Parent = linkNode
 | 
			
		||||
		absoluteLink := isLinkStr(link)
 | 
			
		||||
		if !absoluteLink {
 | 
			
		||||
			if ctx.isWikiMarkdown {
 | 
			
		||||
				link = util.URLJoin("wiki", link)
 | 
			
		||||
			if image {
 | 
			
		||||
				link = strings.ReplaceAll(link, " ", "+")
 | 
			
		||||
			} else {
 | 
			
		||||
				link = strings.ReplaceAll(link, " ", "-")
 | 
			
		||||
			}
 | 
			
		||||
			if !strings.Contains(link, "/") {
 | 
			
		||||
				link = url.PathEscape(link)
 | 
			
		||||
			}
 | 
			
		||||
			link = util.URLJoin(urlPrefix, link)
 | 
			
		||||
		}
 | 
			
		||||
		childNode.Type = html.TextNode
 | 
			
		||||
		childNode.Data = name
 | 
			
		||||
		urlPrefix := ctx.urlPrefix
 | 
			
		||||
		if image {
 | 
			
		||||
			if !absoluteLink {
 | 
			
		||||
				if IsSameDomain(urlPrefix) {
 | 
			
		||||
					urlPrefix = strings.Replace(urlPrefix, "/src/", "/raw/", 1)
 | 
			
		||||
				}
 | 
			
		||||
				if ctx.isWikiMarkdown {
 | 
			
		||||
					link = util.URLJoin("wiki", "raw", link)
 | 
			
		||||
				}
 | 
			
		||||
				link = util.URLJoin(urlPrefix, link)
 | 
			
		||||
			}
 | 
			
		||||
			title := props["title"]
 | 
			
		||||
			if title == "" {
 | 
			
		||||
				title = props["alt"]
 | 
			
		||||
			}
 | 
			
		||||
			if title == "" {
 | 
			
		||||
				title = path.Base(name)
 | 
			
		||||
			}
 | 
			
		||||
			alt := props["alt"]
 | 
			
		||||
			if alt == "" {
 | 
			
		||||
				alt = name
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// make the childNode an image - if we can, we also place the alt
 | 
			
		||||
			childNode.Type = html.ElementNode
 | 
			
		||||
			childNode.Data = "img"
 | 
			
		||||
			childNode.DataAtom = atom.Img
 | 
			
		||||
			childNode.Attr = []html.Attribute{
 | 
			
		||||
				{Key: "src", Val: link},
 | 
			
		||||
				{Key: "title", Val: title},
 | 
			
		||||
				{Key: "alt", Val: alt},
 | 
			
		||||
			}
 | 
			
		||||
			if alt == "" {
 | 
			
		||||
				childNode.Attr = childNode.Attr[:2]
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if !absoluteLink {
 | 
			
		||||
				if ctx.isWikiMarkdown {
 | 
			
		||||
					link = util.URLJoin("wiki", link)
 | 
			
		||||
				}
 | 
			
		||||
				link = util.URLJoin(urlPrefix, link)
 | 
			
		||||
			}
 | 
			
		||||
			childNode.Type = html.TextNode
 | 
			
		||||
			childNode.Data = name
 | 
			
		||||
		}
 | 
			
		||||
		if noLink {
 | 
			
		||||
			linkNode = childNode
 | 
			
		||||
		} else {
 | 
			
		||||
			linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
 | 
			
		||||
		}
 | 
			
		||||
		replaceContent(node, m[0], m[1], linkNode)
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
	if noLink {
 | 
			
		||||
		linkNode = childNode
 | 
			
		||||
	} else {
 | 
			
		||||
		linkNode.Attr = []html.Attribute{{Key: "href", Val: link}}
 | 
			
		||||
	}
 | 
			
		||||
	replaceContent(node, m[0], m[1], linkNode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func fullIssuePatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	link := node.Data[m[0]:m[1]]
 | 
			
		||||
	id := "#" + node.Data[m[2]:m[3]]
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		m := getIssueFullPattern().FindStringSubmatchIndex(node.Data)
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		link := node.Data[m[0]:m[1]]
 | 
			
		||||
		id := "#" + node.Data[m[2]:m[3]]
 | 
			
		||||
 | 
			
		||||
	// extract repo and org name from matched link like
 | 
			
		||||
	// http://localhost:3000/gituser/myrepo/issues/1
 | 
			
		||||
	linkParts := strings.Split(path.Clean(link), "/")
 | 
			
		||||
	matchOrg := linkParts[len(linkParts)-4]
 | 
			
		||||
	matchRepo := linkParts[len(linkParts)-3]
 | 
			
		||||
		// extract repo and org name from matched link like
 | 
			
		||||
		// http://localhost:3000/gituser/myrepo/issues/1
 | 
			
		||||
		linkParts := strings.Split(path.Clean(link), "/")
 | 
			
		||||
		matchOrg := linkParts[len(linkParts)-4]
 | 
			
		||||
		matchRepo := linkParts[len(linkParts)-3]
 | 
			
		||||
 | 
			
		||||
	if matchOrg == ctx.metas["user"] && matchRepo == ctx.metas["repo"] {
 | 
			
		||||
		// TODO if m[4]:m[5] is not nil, then link is to a comment,
 | 
			
		||||
		// and we should indicate that in the text somehow
 | 
			
		||||
		replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue"))
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		orgRepoID := matchOrg + "/" + matchRepo + id
 | 
			
		||||
		replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue"))
 | 
			
		||||
		if matchOrg == ctx.metas["user"] && matchRepo == ctx.metas["repo"] {
 | 
			
		||||
			// TODO if m[4]:m[5] is not nil, then link is to a comment,
 | 
			
		||||
			// and we should indicate that in the text somehow
 | 
			
		||||
			replaceContent(node, m[0], m[1], createLink(link, id, "ref-issue"))
 | 
			
		||||
		} else {
 | 
			
		||||
			orgRepoID := matchOrg + "/" + matchRepo + id
 | 
			
		||||
			replaceContent(node, m[0], m[1], createLink(link, orgRepoID, "ref-issue"))
 | 
			
		||||
		}
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -826,70 +841,74 @@ func issueIndexPatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		found bool
 | 
			
		||||
		ref   *references.RenderizableReference
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	_, exttrack := ctx.metas["format"]
 | 
			
		||||
	alphanum := ctx.metas["style"] == IssueNameStyleAlphanumeric
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		_, exttrack := ctx.metas["format"]
 | 
			
		||||
		alphanum := ctx.metas["style"] == IssueNameStyleAlphanumeric
 | 
			
		||||
 | 
			
		||||
	// Repos with external issue trackers might still need to reference local PRs
 | 
			
		||||
	// We need to concern with the first one that shows up in the text, whichever it is
 | 
			
		||||
	found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
 | 
			
		||||
	if exttrack && alphanum {
 | 
			
		||||
		if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
 | 
			
		||||
			if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
 | 
			
		||||
				found = true
 | 
			
		||||
				ref = ref2
 | 
			
		||||
		// Repos with external issue trackers might still need to reference local PRs
 | 
			
		||||
		// We need to concern with the first one that shows up in the text, whichever it is
 | 
			
		||||
		found, ref = references.FindRenderizableReferenceNumeric(node.Data, exttrack && alphanum)
 | 
			
		||||
		if exttrack && alphanum {
 | 
			
		||||
			if found2, ref2 := references.FindRenderizableReferenceAlphanumeric(node.Data); found2 {
 | 
			
		||||
				if !found || ref2.RefLocation.Start < ref.RefLocation.Start {
 | 
			
		||||
					found = true
 | 
			
		||||
					ref = ref2
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !found {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var link *html.Node
 | 
			
		||||
	reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
 | 
			
		||||
	if exttrack && !ref.IsPull {
 | 
			
		||||
		ctx.metas["index"] = ref.Issue
 | 
			
		||||
		link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "ref-issue")
 | 
			
		||||
	} else {
 | 
			
		||||
		// Path determines the type of link that will be rendered. It's unknown at this point whether
 | 
			
		||||
		// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
 | 
			
		||||
		// Gitea will redirect on click as appropriate.
 | 
			
		||||
		path := "issues"
 | 
			
		||||
		if ref.IsPull {
 | 
			
		||||
			path = "pulls"
 | 
			
		||||
		if !found {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if ref.Owner == "" {
 | 
			
		||||
			link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "ref-issue")
 | 
			
		||||
 | 
			
		||||
		var link *html.Node
 | 
			
		||||
		reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
 | 
			
		||||
		if exttrack && !ref.IsPull {
 | 
			
		||||
			ctx.metas["index"] = ref.Issue
 | 
			
		||||
			link = createLink(com.Expand(ctx.metas["format"], ctx.metas), reftext, "ref-issue")
 | 
			
		||||
		} else {
 | 
			
		||||
			link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
 | 
			
		||||
			// Path determines the type of link that will be rendered. It's unknown at this point whether
 | 
			
		||||
			// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
 | 
			
		||||
			// Gitea will redirect on click as appropriate.
 | 
			
		||||
			path := "issues"
 | 
			
		||||
			if ref.IsPull {
 | 
			
		||||
				path = "pulls"
 | 
			
		||||
			}
 | 
			
		||||
			if ref.Owner == "" {
 | 
			
		||||
				link = createLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], path, ref.Issue), reftext, "ref-issue")
 | 
			
		||||
			} else {
 | 
			
		||||
				link = createLink(util.URLJoin(setting.AppURL, ref.Owner, ref.Name, path, ref.Issue), reftext, "ref-issue")
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ref.Action == references.XRefActionNone {
 | 
			
		||||
		replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
		if ref.Action == references.XRefActionNone {
 | 
			
		||||
			replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
 | 
			
		||||
			node = node.NextSibling.NextSibling
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	// Decorate action keywords if actionable
 | 
			
		||||
	var keyword *html.Node
 | 
			
		||||
	if references.IsXrefActionable(ref, exttrack, alphanum) {
 | 
			
		||||
		keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
 | 
			
		||||
	} else {
 | 
			
		||||
		keyword = &html.Node{
 | 
			
		||||
		// Decorate action keywords if actionable
 | 
			
		||||
		var keyword *html.Node
 | 
			
		||||
		if references.IsXrefActionable(ref, exttrack, alphanum) {
 | 
			
		||||
			keyword = createKeyword(node.Data[ref.ActionLocation.Start:ref.ActionLocation.End])
 | 
			
		||||
		} else {
 | 
			
		||||
			keyword = &html.Node{
 | 
			
		||||
				Type: html.TextNode,
 | 
			
		||||
				Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		spaces := &html.Node{
 | 
			
		||||
			Type: html.TextNode,
 | 
			
		||||
			Data: node.Data[ref.ActionLocation.Start:ref.ActionLocation.End],
 | 
			
		||||
			Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
 | 
			
		||||
		}
 | 
			
		||||
		replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
 | 
			
		||||
		node = node.NextSibling.NextSibling.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
	spaces := &html.Node{
 | 
			
		||||
		Type: html.TextNode,
 | 
			
		||||
		Data: node.Data[ref.ActionLocation.End:ref.RefLocation.Start],
 | 
			
		||||
	}
 | 
			
		||||
	replaceContentList(node, ref.ActionLocation.Start, ref.RefLocation.End, []*html.Node{keyword, spaces, link})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// fullSha1PatternProcessor renders SHA containing URLs
 | 
			
		||||
@@ -897,87 +916,112 @@ func fullSha1PatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	m := anySHA1Pattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	urlFull := node.Data[m[0]:m[1]]
 | 
			
		||||
	text := base.ShortSha(node.Data[m[2]:m[3]])
 | 
			
		||||
 | 
			
		||||
	// 3rd capture group matches a optional path
 | 
			
		||||
	subpath := ""
 | 
			
		||||
	if m[5] > 0 {
 | 
			
		||||
		subpath = node.Data[m[4]:m[5]]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 4th capture group matches a optional url hash
 | 
			
		||||
	hash := ""
 | 
			
		||||
	if m[7] > 0 {
 | 
			
		||||
		hash = node.Data[m[6]:m[7]][1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	start := m[0]
 | 
			
		||||
	end := m[1]
 | 
			
		||||
 | 
			
		||||
	// If url ends in '.', it's very likely that it is not part of the
 | 
			
		||||
	// actual url but used to finish a sentence.
 | 
			
		||||
	if strings.HasSuffix(urlFull, ".") {
 | 
			
		||||
		end--
 | 
			
		||||
		urlFull = urlFull[:len(urlFull)-1]
 | 
			
		||||
		if hash != "" {
 | 
			
		||||
			hash = hash[:len(hash)-1]
 | 
			
		||||
		} else if subpath != "" {
 | 
			
		||||
			subpath = subpath[:len(subpath)-1]
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		m := anySHA1Pattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if subpath != "" {
 | 
			
		||||
		text += subpath
 | 
			
		||||
	}
 | 
			
		||||
		urlFull := node.Data[m[0]:m[1]]
 | 
			
		||||
		text := base.ShortSha(node.Data[m[2]:m[3]])
 | 
			
		||||
 | 
			
		||||
	if hash != "" {
 | 
			
		||||
		text += " (" + hash + ")"
 | 
			
		||||
	}
 | 
			
		||||
		// 3rd capture group matches a optional path
 | 
			
		||||
		subpath := ""
 | 
			
		||||
		if m[5] > 0 {
 | 
			
		||||
			subpath = node.Data[m[4]:m[5]]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
 | 
			
		||||
		// 4th capture group matches a optional url hash
 | 
			
		||||
		hash := ""
 | 
			
		||||
		if m[7] > 0 {
 | 
			
		||||
			hash = node.Data[m[6]:m[7]][1:]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		start := m[0]
 | 
			
		||||
		end := m[1]
 | 
			
		||||
 | 
			
		||||
		// If url ends in '.', it's very likely that it is not part of the
 | 
			
		||||
		// actual url but used to finish a sentence.
 | 
			
		||||
		if strings.HasSuffix(urlFull, ".") {
 | 
			
		||||
			end--
 | 
			
		||||
			urlFull = urlFull[:len(urlFull)-1]
 | 
			
		||||
			if hash != "" {
 | 
			
		||||
				hash = hash[:len(hash)-1]
 | 
			
		||||
			} else if subpath != "" {
 | 
			
		||||
				subpath = subpath[:len(subpath)-1]
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if subpath != "" {
 | 
			
		||||
			text += subpath
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if hash != "" {
 | 
			
		||||
			text += " (" + hash + ")"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// emojiShortCodeProcessor for rendering text like :smile: into emoji
 | 
			
		||||
func emojiShortCodeProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
 | 
			
		||||
	m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	alias := node.Data[m[0]:m[1]]
 | 
			
		||||
	alias = strings.ReplaceAll(alias, ":", "")
 | 
			
		||||
	converted := emoji.FromAlias(alias)
 | 
			
		||||
	if converted == nil {
 | 
			
		||||
		// check if this is a custom reaction
 | 
			
		||||
		s := strings.Join(setting.UI.Reactions, " ") + "gitea"
 | 
			
		||||
		if strings.Contains(s, alias) {
 | 
			
		||||
			replaceContent(node, m[0], m[1], createCustomEmoji(alias, "emoji"))
 | 
			
		||||
	start := 0
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next && start < len(node.Data) {
 | 
			
		||||
		m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
		m[0] += start
 | 
			
		||||
		m[1] += start
 | 
			
		||||
 | 
			
		||||
	replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
 | 
			
		||||
		start = m[1]
 | 
			
		||||
 | 
			
		||||
		alias := node.Data[m[0]:m[1]]
 | 
			
		||||
		alias = strings.ReplaceAll(alias, ":", "")
 | 
			
		||||
		converted := emoji.FromAlias(alias)
 | 
			
		||||
		if converted == nil {
 | 
			
		||||
			// check if this is a custom reaction
 | 
			
		||||
			s := strings.Join(setting.UI.Reactions, " ") + "gitea"
 | 
			
		||||
			if strings.Contains(s, alias) {
 | 
			
		||||
				replaceContent(node, m[0], m[1], createCustomEmoji(alias, "emoji"))
 | 
			
		||||
				node = node.NextSibling.NextSibling
 | 
			
		||||
				start = 0
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
		start = 0
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// emoji processor to match emoji and add emoji class
 | 
			
		||||
func emojiProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	m := emoji.FindEmojiSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	start := 0
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next && start < len(node.Data) {
 | 
			
		||||
		m := emoji.FindEmojiSubmatchIndex(node.Data[start:])
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		m[0] += start
 | 
			
		||||
		m[1] += start
 | 
			
		||||
 | 
			
		||||
	codepoint := node.Data[m[0]:m[1]]
 | 
			
		||||
	val := emoji.FromCode(codepoint)
 | 
			
		||||
	if val != nil {
 | 
			
		||||
		replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
 | 
			
		||||
		codepoint := node.Data[m[0]:m[1]]
 | 
			
		||||
		start = m[1]
 | 
			
		||||
		val := emoji.FromCode(codepoint)
 | 
			
		||||
		if val != nil {
 | 
			
		||||
			replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
 | 
			
		||||
			node = node.NextSibling.NextSibling
 | 
			
		||||
			start = 0
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -987,49 +1031,69 @@ func sha1CurrentPatternProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	if ctx.metas == nil || ctx.metas["user"] == "" || ctx.metas["repo"] == "" || ctx.metas["repoPath"] == "" {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	hash := node.Data[m[2]:m[3]]
 | 
			
		||||
	// The regex does not lie, it matches the hash pattern.
 | 
			
		||||
	// However, a regex cannot know if a hash actually exists or not.
 | 
			
		||||
	// We could assume that a SHA1 hash should probably contain alphas AND numerics
 | 
			
		||||
	// but that is not always the case.
 | 
			
		||||
	// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
 | 
			
		||||
	// as used by git and github for linking and thus we have to do similar.
 | 
			
		||||
	// Because of this, we check to make sure that a matched hash is actually
 | 
			
		||||
	// a commit in the repository before making it a link.
 | 
			
		||||
	if _, err := git.NewCommand("rev-parse", "--verify", hash).RunInDirBytes(ctx.metas["repoPath"]); err != nil {
 | 
			
		||||
		if !strings.Contains(err.Error(), "fatal: Needed a single revision") {
 | 
			
		||||
			log.Debug("sha1CurrentPatternProcessor git rev-parse: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	replaceContent(node, m[2], m[3],
 | 
			
		||||
		createCodeLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "commit", hash), base.ShortSha(hash), "commit"))
 | 
			
		||||
	start := 0
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next && start < len(node.Data) {
 | 
			
		||||
		m := sha1CurrentPattern.FindStringSubmatchIndex(node.Data[start:])
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		m[2] += start
 | 
			
		||||
		m[3] += start
 | 
			
		||||
 | 
			
		||||
		hash := node.Data[m[2]:m[3]]
 | 
			
		||||
		// The regex does not lie, it matches the hash pattern.
 | 
			
		||||
		// However, a regex cannot know if a hash actually exists or not.
 | 
			
		||||
		// We could assume that a SHA1 hash should probably contain alphas AND numerics
 | 
			
		||||
		// but that is not always the case.
 | 
			
		||||
		// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
 | 
			
		||||
		// as used by git and github for linking and thus we have to do similar.
 | 
			
		||||
		// Because of this, we check to make sure that a matched hash is actually
 | 
			
		||||
		// a commit in the repository before making it a link.
 | 
			
		||||
		if _, err := git.NewCommand("rev-parse", "--verify", hash).RunInDirBytes(ctx.metas["repoPath"]); err != nil {
 | 
			
		||||
			if !strings.Contains(err.Error(), "fatal: Needed a single revision") {
 | 
			
		||||
				log.Debug("sha1CurrentPatternProcessor git rev-parse: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
			start = m[3]
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		replaceContent(node, m[2], m[3],
 | 
			
		||||
			createCodeLink(util.URLJoin(setting.AppURL, ctx.metas["user"], ctx.metas["repo"], "commit", hash), base.ShortSha(hash), "commit"))
 | 
			
		||||
		start = 0
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// emailAddressProcessor replaces raw email addresses with a mailto: link.
 | 
			
		||||
func emailAddressProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	m := emailRegex.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		m := emailRegex.FindStringSubmatchIndex(node.Data)
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		mail := node.Data[m[2]:m[3]]
 | 
			
		||||
		replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto"))
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
	mail := node.Data[m[2]:m[3]]
 | 
			
		||||
	replaceContent(node, m[2], m[3], createLink("mailto:"+mail, mail, "mailto"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// linkProcessor creates links for any HTTP or HTTPS URL not captured by
 | 
			
		||||
// markdown.
 | 
			
		||||
func linkProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	m := common.LinkRegex.FindStringIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		m := common.LinkRegex.FindStringIndex(node.Data)
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		uri := node.Data[m[0]:m[1]]
 | 
			
		||||
		replaceContent(node, m[0], m[1], createLink(uri, uri, "link"))
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
	uri := node.Data[m[0]:m[1]]
 | 
			
		||||
	replaceContent(node, m[0], m[1], createLink(uri, uri, "link"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func genDefaultLinkProcessor(defaultLink string) processor {
 | 
			
		||||
@@ -1053,12 +1117,17 @@ func genDefaultLinkProcessor(defaultLink string) processor {
 | 
			
		||||
 | 
			
		||||
// descriptionLinkProcessor creates links for DescriptionHTML
 | 
			
		||||
func descriptionLinkProcessor(ctx *postProcessCtx, node *html.Node) {
 | 
			
		||||
	m := common.LinkRegex.FindStringIndex(node.Data)
 | 
			
		||||
	if m == nil {
 | 
			
		||||
		return
 | 
			
		||||
	next := node.NextSibling
 | 
			
		||||
	for node != nil && node != next {
 | 
			
		||||
		m := common.LinkRegex.FindStringIndex(node.Data)
 | 
			
		||||
		if m == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		uri := node.Data[m[0]:m[1]]
 | 
			
		||||
		replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri))
 | 
			
		||||
		node = node.NextSibling.NextSibling
 | 
			
		||||
	}
 | 
			
		||||
	uri := node.Data[m[0]:m[1]]
 | 
			
		||||
	replaceContent(node, m[0], m[1], createDescriptionLink(uri, uri))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createDescriptionLink(href, content string) *html.Node {
 | 
			
		||||
 
 | 
			
		||||
@@ -408,3 +408,36 @@ func Test_ParseClusterFuzz(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	assert.NotContains(t, string(val), "<html")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestIssue16020(t *testing.T) {
 | 
			
		||||
	setting.AppURL = AppURL
 | 
			
		||||
	setting.AppSubURL = AppSubURL
 | 
			
		||||
 | 
			
		||||
	var localMetas = map[string]string{
 | 
			
		||||
		"user": "go-gitea",
 | 
			
		||||
		"repo": "gitea",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data := `<img src="data:image/png;base64,i//V"/>`
 | 
			
		||||
 | 
			
		||||
	// func PostProcess(rawHTML []byte, urlPrefix string, metas map[string]string, isWikiMarkdown bool) ([]byte, error)
 | 
			
		||||
	res, err := PostProcess([]byte(data), "https://example.com", localMetas, false)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, data, string(res))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkEmojiPostprocess(b *testing.B) {
 | 
			
		||||
	data := "🥰 "
 | 
			
		||||
	for len(data) < 1<<16 {
 | 
			
		||||
		data += data
 | 
			
		||||
	}
 | 
			
		||||
	b.ResetTimer()
 | 
			
		||||
	for i := 0; i < b.N; i++ {
 | 
			
		||||
		_, err := PostProcess(
 | 
			
		||||
			[]byte(data),
 | 
			
		||||
			"https://example.com",
 | 
			
		||||
			localMetas,
 | 
			
		||||
			false)
 | 
			
		||||
		assert.NoError(b, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -50,9 +50,6 @@ func ReplaceSanitizer() {
 | 
			
		||||
		sanitizer.policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Allow keyword markup
 | 
			
		||||
	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^` + keywordClass + `$`)).OnElements("span")
 | 
			
		||||
 | 
			
		||||
	// Allow classes for anchors
 | 
			
		||||
	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue`)).OnElements("a")
 | 
			
		||||
 | 
			
		||||
@@ -68,8 +65,8 @@ func ReplaceSanitizer() {
 | 
			
		||||
	// Allow classes for emojis
 | 
			
		||||
	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
 | 
			
		||||
 | 
			
		||||
	// Allow icons, emojis, and chroma syntax on span
 | 
			
		||||
	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$`)).OnElements("span")
 | 
			
		||||
	// Allow icons, emojis, chroma syntax and keyword markup on span
 | 
			
		||||
	sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
 | 
			
		||||
 | 
			
		||||
	// Allow generally safe attributes
 | 
			
		||||
	generalSafeAttrs := []string{"abbr", "accept", "accept-charset",
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,13 @@ import "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
// this is for internal usage by migrations module and func who interact with it
 | 
			
		||||
type MigrateOptions struct {
 | 
			
		||||
	// required: true
 | 
			
		||||
	CloneAddr    string `json:"clone_addr" binding:"Required"`
 | 
			
		||||
	AuthUsername string `json:"auth_username"`
 | 
			
		||||
	AuthPassword string `json:"auth_password"`
 | 
			
		||||
	AuthToken    string `json:"auth_token"`
 | 
			
		||||
	CloneAddr             string `json:"clone_addr" binding:"Required"`
 | 
			
		||||
	CloneAddrEncrypted    string `json:"clone_addr_encrypted,omitempty"`
 | 
			
		||||
	AuthUsername          string `json:"auth_username"`
 | 
			
		||||
	AuthPassword          string `json:"auth_password,omitempty"`
 | 
			
		||||
	AuthPasswordEncrypted string `json:"auth_password_encrypted,omitempty"`
 | 
			
		||||
	AuthToken             string `json:"auth_token,omitempty"`
 | 
			
		||||
	AuthTokenEncrypted    string `json:"auth_token_encrypted,omitempty"`
 | 
			
		||||
	// required: true
 | 
			
		||||
	UID int `json:"uid" binding:"Required"`
 | 
			
		||||
	// required: true
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
@@ -563,8 +564,42 @@ func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.Mi
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func updateOptionsUnits(opts *base.MigrateOptions, units []string) {
 | 
			
		||||
	if len(units) == 0 {
 | 
			
		||||
		opts.Wiki = true
 | 
			
		||||
		opts.Issues = true
 | 
			
		||||
		opts.Milestones = true
 | 
			
		||||
		opts.Labels = true
 | 
			
		||||
		opts.Releases = true
 | 
			
		||||
		opts.Comments = true
 | 
			
		||||
		opts.PullRequests = true
 | 
			
		||||
		opts.ReleaseAssets = true
 | 
			
		||||
	} else {
 | 
			
		||||
		for _, unit := range units {
 | 
			
		||||
			switch strings.ToLower(unit) {
 | 
			
		||||
			case "wiki":
 | 
			
		||||
				opts.Wiki = true
 | 
			
		||||
			case "issues":
 | 
			
		||||
				opts.Issues = true
 | 
			
		||||
			case "milestones":
 | 
			
		||||
				opts.Milestones = true
 | 
			
		||||
			case "labels":
 | 
			
		||||
				opts.Labels = true
 | 
			
		||||
			case "releases":
 | 
			
		||||
				opts.Releases = true
 | 
			
		||||
			case "release_assets":
 | 
			
		||||
				opts.ReleaseAssets = true
 | 
			
		||||
			case "comments":
 | 
			
		||||
				opts.Comments = true
 | 
			
		||||
			case "pull_requests":
 | 
			
		||||
				opts.PullRequests = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RestoreRepository restore a repository from the disk directory
 | 
			
		||||
func RestoreRepository(ctx context.Context, baseDir string, ownerName, repoName string) error {
 | 
			
		||||
func RestoreRepository(ctx context.Context, baseDir string, ownerName, repoName string, units []string) error {
 | 
			
		||||
	doer, err := models.GetAdminUser()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@@ -580,17 +615,12 @@ func RestoreRepository(ctx context.Context, baseDir string, ownerName, repoName
 | 
			
		||||
	}
 | 
			
		||||
	tp, _ := strconv.Atoi(opts["service_type"])
 | 
			
		||||
 | 
			
		||||
	if err = migrateRepository(downloader, uploader, base.MigrateOptions{
 | 
			
		||||
		Wiki:           true,
 | 
			
		||||
		Issues:         true,
 | 
			
		||||
		Milestones:     true,
 | 
			
		||||
		Labels:         true,
 | 
			
		||||
		Releases:       true,
 | 
			
		||||
		Comments:       true,
 | 
			
		||||
		PullRequests:   true,
 | 
			
		||||
		ReleaseAssets:  true,
 | 
			
		||||
	var migrateOpts = base.MigrateOptions{
 | 
			
		||||
		GitServiceType: structs.GitServiceType(tp),
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
	}
 | 
			
		||||
	updateOptionsUnits(&migrateOpts, units)
 | 
			
		||||
 | 
			
		||||
	if err = migrateRepository(downloader, uploader, migrateOpts); err != nil {
 | 
			
		||||
		if err1 := uploader.Rollback(); err1 != nil {
 | 
			
		||||
			log.Error("rollback failed: %v", err1)
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -248,14 +248,16 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
 | 
			
		||||
			rel.OriginalAuthorID = release.PublisherID
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// calc NumCommits
 | 
			
		||||
		commit, err := g.gitRepo.GetCommit(rel.TagName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("GetCommit: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		rel.NumCommits, err = commit.CommitsCount()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("CommitsCount: %v", err)
 | 
			
		||||
		// calc NumCommits if no draft
 | 
			
		||||
		if !release.Draft {
 | 
			
		||||
			commit, err := g.gitRepo.GetCommit(rel.TagName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("GetCommit: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
			rel.NumCommits, err = commit.CommitsCount()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return fmt.Errorf("CommitsCount: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, asset := range release.Assets {
 | 
			
		||||
@@ -268,9 +270,10 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// download attachment
 | 
			
		||||
			err = func() error {
 | 
			
		||||
			err := func() error {
 | 
			
		||||
				// asset.DownloadURL maybe a local file
 | 
			
		||||
				var rc io.ReadCloser
 | 
			
		||||
				var err error
 | 
			
		||||
				if asset.DownloadURL == nil {
 | 
			
		||||
					rc, err = asset.DownloadFunc()
 | 
			
		||||
					if err != nil {
 | 
			
		||||
@@ -849,6 +852,7 @@ func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
 | 
			
		||||
// Rollback when migrating failed, this will rollback all the changes.
 | 
			
		||||
func (g *GiteaLocalUploader) Rollback() error {
 | 
			
		||||
	if g.repo != nil && g.repo.ID > 0 {
 | 
			
		||||
		g.gitRepo.Close()
 | 
			
		||||
		if err := models.DeleteRepository(g.doer, g.repo.OwnerID, g.repo.ID); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -264,34 +264,29 @@ func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release {
 | 
			
		||||
	var (
 | 
			
		||||
		name string
 | 
			
		||||
		desc string
 | 
			
		||||
	)
 | 
			
		||||
	if rel.Body != nil {
 | 
			
		||||
		desc = *rel.Body
 | 
			
		||||
	}
 | 
			
		||||
	if rel.Name != nil {
 | 
			
		||||
		name = *rel.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var email string
 | 
			
		||||
	if rel.Author.Email != nil {
 | 
			
		||||
		email = *rel.Author.Email
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r := &base.Release{
 | 
			
		||||
		TagName:         *rel.TagName,
 | 
			
		||||
		TargetCommitish: *rel.TargetCommitish,
 | 
			
		||||
		Name:            name,
 | 
			
		||||
		Body:            desc,
 | 
			
		||||
		Draft:           *rel.Draft,
 | 
			
		||||
		Prerelease:      *rel.Prerelease,
 | 
			
		||||
		Created:         rel.CreatedAt.Time,
 | 
			
		||||
		PublisherID:     *rel.Author.ID,
 | 
			
		||||
		PublisherName:   *rel.Author.Login,
 | 
			
		||||
		PublisherEmail:  email,
 | 
			
		||||
		Published:       rel.PublishedAt.Time,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rel.Body != nil {
 | 
			
		||||
		r.Body = *rel.Body
 | 
			
		||||
	}
 | 
			
		||||
	if rel.Name != nil {
 | 
			
		||||
		r.Name = *rel.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rel.Author.Email != nil {
 | 
			
		||||
		r.PublisherEmail = *rel.Author.Email
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rel.PublishedAt != nil {
 | 
			
		||||
		r.Published = rel.PublishedAt.Time
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, asset := range rel.Assets {
 | 
			
		||||
@@ -306,18 +301,17 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease)
 | 
			
		||||
			Updated:       asset.UpdatedAt.Time,
 | 
			
		||||
			DownloadFunc: func() (io.ReadCloser, error) {
 | 
			
		||||
				g.sleep()
 | 
			
		||||
				asset, redir, err := g.client.Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
 | 
			
		||||
				asset, redirectURL, err := g.client.Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, err
 | 
			
		||||
				}
 | 
			
		||||
				err = g.RefreshRate()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
				if err := g.RefreshRate(); err != nil {
 | 
			
		||||
					log.Error("g.client.RateLimits: %s", err)
 | 
			
		||||
				}
 | 
			
		||||
				if asset == nil {
 | 
			
		||||
					if redir != "" {
 | 
			
		||||
					if redirectURL != "" {
 | 
			
		||||
						g.sleep()
 | 
			
		||||
						req, err := http.NewRequestWithContext(g.ctx, "GET", redir, nil)
 | 
			
		||||
						req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							return nil, err
 | 
			
		||||
						}
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,7 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
 | 
			
		||||
			opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
 | 
			
		||||
		}
 | 
			
		||||
		if uri.Path != "" {
 | 
			
		||||
			if db, err := strconv.Atoi(uri.Path); err == nil {
 | 
			
		||||
			if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
 | 
			
		||||
				opts.DB = db
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -168,7 +168,7 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
 | 
			
		||||
			opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
 | 
			
		||||
		}
 | 
			
		||||
		if uri.Path != "" {
 | 
			
		||||
			if db, err := strconv.Atoi(uri.Path); err == nil {
 | 
			
		||||
			if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
 | 
			
		||||
				opts.DB = db
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@@ -186,7 +186,7 @@ func (m *Manager) GetRedisClient(connection string) redis.UniversalClient {
 | 
			
		||||
			opts.Addrs = append(opts.Addrs, strings.Split(uri.Host, ",")...)
 | 
			
		||||
		}
 | 
			
		||||
		if uri.Path != "" {
 | 
			
		||||
			if db, err := strconv.Atoi(uri.Path); err == nil {
 | 
			
		||||
			if db, err := strconv.Atoi(uri.Path[1:]); err == nil {
 | 
			
		||||
				opts.DB = db
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,6 @@ func (m *mailNotifier) NotifyNewIssue(issue *models.Issue, mentions []*models.Us
 | 
			
		||||
 | 
			
		||||
func (m *mailNotifier) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, actionComment *models.Comment, isClosed bool) {
 | 
			
		||||
	var actionType models.ActionType
 | 
			
		||||
	issue.Content = ""
 | 
			
		||||
	if issue.IsPull {
 | 
			
		||||
		if isClosed {
 | 
			
		||||
			actionType = models.ActionClosePullRequest
 | 
			
		||||
@@ -120,7 +119,6 @@ func (m *mailNotifier) NotifyMergePullRequest(pr *models.PullRequest, doer *mode
 | 
			
		||||
		log.Error("pr.LoadIssue: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	pr.Issue.Content = ""
 | 
			
		||||
	if err := mailer.MailParticipants(pr.Issue, doer, models.ActionMergePullRequest, nil); err != nil {
 | 
			
		||||
		log.Error("MailParticipants: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -147,7 +145,6 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *models.User, pr *model
 | 
			
		||||
	if err := comment.LoadPushCommits(); err != nil {
 | 
			
		||||
		log.Error("comment.LoadPushCommits: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	comment.Content = ""
 | 
			
		||||
 | 
			
		||||
	m.NotifyCreateIssueComment(doer, comment.Issue.Repo, comment.Issue, comment, nil)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								modules/private/restore_repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								modules/private/restore_repo.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
// Copyright 2020 The Gitea Authors. All rights reserved.
 | 
			
		||||
// Use of this source code is governed by a MIT-style
 | 
			
		||||
// license that can be found in the LICENSE file.
 | 
			
		||||
 | 
			
		||||
package private
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	jsoniter "github.com/json-iterator/go"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RestoreParams structure holds a data for restore repository
 | 
			
		||||
type RestoreParams struct {
 | 
			
		||||
	RepoDir   string
 | 
			
		||||
	OwnerName string
 | 
			
		||||
	RepoName  string
 | 
			
		||||
	Units     []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RestoreRepo calls the internal RestoreRepo function
 | 
			
		||||
func RestoreRepo(repoDir, ownerName, repoName string, units []string) (int, string) {
 | 
			
		||||
	reqURL := setting.LocalURL + "api/internal/restore_repo"
 | 
			
		||||
 | 
			
		||||
	req := newInternalRequest(reqURL, "POST")
 | 
			
		||||
	req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout
 | 
			
		||||
	req = req.Header("Content-Type", "application/json")
 | 
			
		||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
			
		||||
	jsonBytes, _ := json.Marshal(RestoreParams{
 | 
			
		||||
		RepoDir:   repoDir,
 | 
			
		||||
		OwnerName: ownerName,
 | 
			
		||||
		RepoName:  repoName,
 | 
			
		||||
		Units:     units,
 | 
			
		||||
	})
 | 
			
		||||
	req.Body(jsonBytes)
 | 
			
		||||
	resp, err := req.Response()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v, could you confirm it's running?", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != 200 {
 | 
			
		||||
		var ret = struct {
 | 
			
		||||
			Err string `json:"err"`
 | 
			
		||||
		}{}
 | 
			
		||||
		body, err := ioutil.ReadAll(resp.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
		if err := json.Unmarshal(body, &ret); err != nil {
 | 
			
		||||
			return http.StatusInternalServerError, fmt.Sprintf("Response body Unmarshal error: %v", err.Error())
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return http.StatusOK, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName)
 | 
			
		||||
}
 | 
			
		||||
@@ -165,7 +165,7 @@ func (opts *Options) handle(w http.ResponseWriter, req *http.Request, opt *Optio
 | 
			
		||||
		log.Println("[Static] Serving " + file)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if httpcache.HandleEtagCache(req, w, fi) {
 | 
			
		||||
	if httpcache.HandleFileETagCache(req, w, fi) {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -198,17 +198,20 @@ func (m *Manager) FlushAll(baseCtx context.Context, timeout time.Duration) error
 | 
			
		||||
					wg.Done()
 | 
			
		||||
				}(mq)
 | 
			
		||||
			} else {
 | 
			
		||||
				log.Debug("Queue: %s is non-empty but is not flushable - adding 100 millisecond wait", mq.Name)
 | 
			
		||||
				go func() {
 | 
			
		||||
					<-time.After(100 * time.Millisecond)
 | 
			
		||||
					wg.Done()
 | 
			
		||||
				}()
 | 
			
		||||
				log.Debug("Queue: %s is non-empty but is not flushable", mq.Name)
 | 
			
		||||
				wg.Done()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		if allEmpty {
 | 
			
		||||
			log.Debug("All queues are empty")
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		// Ensure there are always at least 100ms between loops but not more if we've actually been doing some flushign
 | 
			
		||||
		// but don't delay cancellation here.
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
		case <-time.After(100 * time.Millisecond):
 | 
			
		||||
		}
 | 
			
		||||
		wg.Wait()
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -114,43 +114,73 @@ func (q *ByteFIFOQueue) Run(atShutdown, atTerminate func(context.Context, func()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (q *ByteFIFOQueue) readToChan() {
 | 
			
		||||
	// handle quick cancels
 | 
			
		||||
	select {
 | 
			
		||||
	case <-q.closed:
 | 
			
		||||
		// tell the pool to shutdown.
 | 
			
		||||
		q.cancel()
 | 
			
		||||
		return
 | 
			
		||||
	default:
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	backOffTime := time.Millisecond * 100
 | 
			
		||||
	maxBackOffTime := time.Second * 3
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-q.closed:
 | 
			
		||||
			// tell the pool to shutdown.
 | 
			
		||||
			q.cancel()
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
			q.lock.Lock()
 | 
			
		||||
			bs, err := q.byteFIFO.Pop()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				q.lock.Unlock()
 | 
			
		||||
				log.Error("%s: %s Error on Pop: %v", q.typ, q.name, err)
 | 
			
		||||
				time.Sleep(time.Millisecond * 100)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
		success, resetBackoff := q.doPop()
 | 
			
		||||
		if resetBackoff {
 | 
			
		||||
			backOffTime = 100 * time.Millisecond
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			if len(bs) == 0 {
 | 
			
		||||
				q.lock.Unlock()
 | 
			
		||||
				time.Sleep(time.Millisecond * 100)
 | 
			
		||||
				continue
 | 
			
		||||
		if success {
 | 
			
		||||
			select {
 | 
			
		||||
			case <-q.closed:
 | 
			
		||||
				// tell the pool to shutdown.
 | 
			
		||||
				q.cancel()
 | 
			
		||||
				return
 | 
			
		||||
			default:
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			data, err := unmarshalAs(bs, q.exemplar)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("%s: %s Failed to unmarshal with error: %v", q.typ, q.name, err)
 | 
			
		||||
				q.lock.Unlock()
 | 
			
		||||
				time.Sleep(time.Millisecond * 100)
 | 
			
		||||
				continue
 | 
			
		||||
		} else {
 | 
			
		||||
			select {
 | 
			
		||||
			case <-q.closed:
 | 
			
		||||
				// tell the pool to shutdown.
 | 
			
		||||
				q.cancel()
 | 
			
		||||
				return
 | 
			
		||||
			case <-time.After(backOffTime):
 | 
			
		||||
			}
 | 
			
		||||
			backOffTime += backOffTime / 2
 | 
			
		||||
			if backOffTime > maxBackOffTime {
 | 
			
		||||
				backOffTime = maxBackOffTime
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.Trace("%s %s: Task found: %#v", q.typ, q.name, data)
 | 
			
		||||
			q.WorkerPool.Push(data)
 | 
			
		||||
			q.lock.Unlock()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (q *ByteFIFOQueue) doPop() (success, resetBackoff bool) {
 | 
			
		||||
	q.lock.Lock()
 | 
			
		||||
	defer q.lock.Unlock()
 | 
			
		||||
	bs, err := q.byteFIFO.Pop()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("%s: %s Error on Pop: %v", q.typ, q.name, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if len(bs) == 0 {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resetBackoff = true
 | 
			
		||||
 | 
			
		||||
	data, err := unmarshalAs(bs, q.exemplar)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("%s: %s Failed to unmarshal with error: %v", q.typ, q.name, err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("%s %s: Task found: %#v", q.typ, q.name, data)
 | 
			
		||||
	q.WorkerPool.Push(data)
 | 
			
		||||
	success = true
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Shutdown processing from this queue
 | 
			
		||||
func (q *ByteFIFOQueue) Shutdown() {
 | 
			
		||||
	log.Trace("%s: %s Shutting down", q.typ, q.name)
 | 
			
		||||
 
 | 
			
		||||
@@ -228,7 +228,7 @@ func ListUnadoptedRepositories(query string, opts *models.ListOptions) ([]string
 | 
			
		||||
					found := false
 | 
			
		||||
				repoLoop:
 | 
			
		||||
					for i, repo := range repos {
 | 
			
		||||
						if repo.Name == name {
 | 
			
		||||
						if repo.LowerName == name {
 | 
			
		||||
							found = true
 | 
			
		||||
							repos = append(repos[:i], repos[i+1:]...)
 | 
			
		||||
							break repoLoop
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,53 @@ import (
 | 
			
		||||
func getHookTemplates() (hookNames, hookTpls, giteaHookTpls []string) {
 | 
			
		||||
	hookNames = []string{"pre-receive", "update", "post-receive"}
 | 
			
		||||
	hookTpls = []string{
 | 
			
		||||
		fmt.Sprintf("#!/usr/bin/env %s\ndata=$(cat)\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" && test -f \"${hook}\" || continue\necho \"${data}\" | \"${hook}\"\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
 | 
			
		||||
		fmt.Sprintf("#!/usr/bin/env %s\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" && test -f \"${hook}\" || continue\n\"${hook}\" $1 $2 $3\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
 | 
			
		||||
		fmt.Sprintf("#!/usr/bin/env %s\ndata=$(cat)\nexitcodes=\"\"\nhookname=$(basename $0)\nGIT_DIR=${GIT_DIR:-$(dirname $0)}\n\nfor hook in ${GIT_DIR}/hooks/${hookname}.d/*; do\ntest -x \"${hook}\" && test -f \"${hook}\" || continue\necho \"${data}\" | \"${hook}\"\nexitcodes=\"${exitcodes} $?\"\ndone\n\nfor i in ${exitcodes}; do\n[ ${i} -eq 0 ] || exit ${i}\ndone\n", setting.ScriptType),
 | 
			
		||||
		fmt.Sprintf(`#!/usr/bin/env %s
 | 
			
		||||
data=$(cat)
 | 
			
		||||
exitcodes=""
 | 
			
		||||
hookname=$(basename $0)
 | 
			
		||||
GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
 | 
			
		||||
 | 
			
		||||
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
 | 
			
		||||
test -x "${hook}" && test -f "${hook}" || continue
 | 
			
		||||
echo "${data}" | "${hook}"
 | 
			
		||||
exitcodes="${exitcodes} $?"
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
for i in ${exitcodes}; do
 | 
			
		||||
[ ${i} -eq 0 ] || exit ${i}
 | 
			
		||||
done
 | 
			
		||||
`, setting.ScriptType),
 | 
			
		||||
		fmt.Sprintf(`#!/usr/bin/env %s
 | 
			
		||||
exitcodes=""
 | 
			
		||||
hookname=$(basename $0)
 | 
			
		||||
GIT_DIR=${GIT_DIR:-$(dirname $0/..)}
 | 
			
		||||
 | 
			
		||||
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
 | 
			
		||||
test -x "${hook}" && test -f "${hook}" || continue
 | 
			
		||||
"${hook}" $1 $2 $3
 | 
			
		||||
exitcodes="${exitcodes} $?"
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
for i in ${exitcodes}; do
 | 
			
		||||
[ ${i} -eq 0 ] || exit ${i}
 | 
			
		||||
done
 | 
			
		||||
`, setting.ScriptType),
 | 
			
		||||
		fmt.Sprintf(`#!/usr/bin/env %s
 | 
			
		||||
data=$(cat)
 | 
			
		||||
exitcodes=""
 | 
			
		||||
hookname=$(basename $0)
 | 
			
		||||
GIT_DIR=${GIT_DIR:-$(dirname $0)/..}
 | 
			
		||||
 | 
			
		||||
for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do
 | 
			
		||||
test -x "${hook}" && test -f "${hook}" || continue
 | 
			
		||||
echo "${data}" | "${hook}"
 | 
			
		||||
exitcodes="${exitcodes} $?"
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
for i in ${exitcodes}; do
 | 
			
		||||
[ ${i} -eq 0 ] || exit ${i}
 | 
			
		||||
done
 | 
			
		||||
`, setting.ScriptType),
 | 
			
		||||
	}
 | 
			
		||||
	giteaHookTpls = []string{
 | 
			
		||||
		fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s pre-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)),
 | 
			
		||||
 
 | 
			
		||||
@@ -117,6 +117,8 @@ var (
 | 
			
		||||
	GracefulRestartable  bool
 | 
			
		||||
	GracefulHammerTime   time.Duration
 | 
			
		||||
	StartupTimeout       time.Duration
 | 
			
		||||
	PerWriteTimeout      = 30 * time.Second
 | 
			
		||||
	PerWritePerKbTimeout = 10 * time.Second
 | 
			
		||||
	StaticURLPrefix      string
 | 
			
		||||
	AbsoluteAssetURL     string
 | 
			
		||||
 | 
			
		||||
@@ -147,18 +149,22 @@ var (
 | 
			
		||||
		TrustedUserCAKeys              []string          `ini:"SSH_TRUSTED_USER_CA_KEYS"`
 | 
			
		||||
		TrustedUserCAKeysFile          string            `ini:"SSH_TRUSTED_USER_CA_KEYS_FILENAME"`
 | 
			
		||||
		TrustedUserCAKeysParsed        []gossh.PublicKey `ini:"-"`
 | 
			
		||||
		PerWriteTimeout                time.Duration     `ini:"SSH_PER_WRITE_TIMEOUT"`
 | 
			
		||||
		PerWritePerKbTimeout           time.Duration     `ini:"SSH_PER_WRITE_PER_KB_TIMEOUT"`
 | 
			
		||||
	}{
 | 
			
		||||
		Disabled:            false,
 | 
			
		||||
		StartBuiltinServer:  false,
 | 
			
		||||
		Domain:              "",
 | 
			
		||||
		Port:                22,
 | 
			
		||||
		ServerCiphers:       []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128"},
 | 
			
		||||
		ServerKeyExchanges:  []string{"diffie-hellman-group1-sha1", "diffie-hellman-group14-sha1", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "curve25519-sha256@libssh.org"},
 | 
			
		||||
		ServerMACs:          []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1", "hmac-sha1-96"},
 | 
			
		||||
		KeygenPath:          "ssh-keygen",
 | 
			
		||||
		MinimumKeySizeCheck: true,
 | 
			
		||||
		MinimumKeySizes:     map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2048},
 | 
			
		||||
		ServerHostKeys:      []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
 | 
			
		||||
		Disabled:             false,
 | 
			
		||||
		StartBuiltinServer:   false,
 | 
			
		||||
		Domain:               "",
 | 
			
		||||
		Port:                 22,
 | 
			
		||||
		ServerCiphers:        []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "arcfour256", "arcfour128"},
 | 
			
		||||
		ServerKeyExchanges:   []string{"diffie-hellman-group1-sha1", "diffie-hellman-group14-sha1", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "curve25519-sha256@libssh.org"},
 | 
			
		||||
		ServerMACs:           []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1", "hmac-sha1-96"},
 | 
			
		||||
		KeygenPath:           "ssh-keygen",
 | 
			
		||||
		MinimumKeySizeCheck:  true,
 | 
			
		||||
		MinimumKeySizes:      map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 2048},
 | 
			
		||||
		ServerHostKeys:       []string{"ssh/gitea.rsa", "ssh/gogs.rsa"},
 | 
			
		||||
		PerWriteTimeout:      PerWriteTimeout,
 | 
			
		||||
		PerWritePerKbTimeout: PerWritePerKbTimeout,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Security settings
 | 
			
		||||
@@ -607,6 +613,8 @@ func NewContext() {
 | 
			
		||||
	GracefulRestartable = sec.Key("ALLOW_GRACEFUL_RESTARTS").MustBool(true)
 | 
			
		||||
	GracefulHammerTime = sec.Key("GRACEFUL_HAMMER_TIME").MustDuration(60 * time.Second)
 | 
			
		||||
	StartupTimeout = sec.Key("STARTUP_TIMEOUT").MustDuration(0 * time.Second)
 | 
			
		||||
	PerWriteTimeout = sec.Key("PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
 | 
			
		||||
	PerWritePerKbTimeout = sec.Key("PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
 | 
			
		||||
 | 
			
		||||
	defaultAppURL := string(Protocol) + "://" + Domain
 | 
			
		||||
	if (Protocol == HTTP && HTTPPort != "80") || (Protocol == HTTPS && HTTPPort != "443") {
 | 
			
		||||
@@ -772,6 +780,8 @@ func NewContext() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	SSH.ExposeAnonymous = sec.Key("SSH_EXPOSE_ANONYMOUS").MustBool(false)
 | 
			
		||||
	SSH.PerWriteTimeout = sec.Key("SSH_PER_WRITE_TIMEOUT").MustDuration(PerWriteTimeout)
 | 
			
		||||
	SSH.PerWritePerKbTimeout = sec.Key("SSH_PER_WRITE_PER_KB_TIMEOUT").MustDuration(PerWritePerKbTimeout)
 | 
			
		||||
 | 
			
		||||
	if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil {
 | 
			
		||||
		log.Fatal("Failed to OAuth2 settings: %v", err)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,15 @@ package ssh
 | 
			
		||||
import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
 | 
			
		||||
	"github.com/gliderlabs/ssh"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func listen(server *ssh.Server) {
 | 
			
		||||
	gracefulServer := graceful.NewServer("tcp", server.Addr, "SSH")
 | 
			
		||||
	gracefulServer.PerWriteTimeout = setting.SSH.PerWriteTimeout
 | 
			
		||||
	gracefulServer.PerWritePerKbTimeout = setting.SSH.PerWritePerKbTimeout
 | 
			
		||||
 | 
			
		||||
	err := gracefulServer.ListenAndServe(server.Serve)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,8 @@ type CreateOrgOption struct {
 | 
			
		||||
	RepoAdminChangeTeamAccess bool   `json:"repo_admin_change_team_access"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged
 | 
			
		||||
 | 
			
		||||
// EditOrgOption options for editing an organization
 | 
			
		||||
type EditOrgOption struct {
 | 
			
		||||
	FullName    string `json:"full_name"`
 | 
			
		||||
@@ -40,5 +42,5 @@ type EditOrgOption struct {
 | 
			
		||||
	// possible values are `public`, `limited` or `private`
 | 
			
		||||
	// enum: public,limited,private
 | 
			
		||||
	Visibility                string `json:"visibility" binding:"In(,public,limited,private)"`
 | 
			
		||||
	RepoAdminChangeTeamAccess bool   `json:"repo_admin_change_team_access"`
 | 
			
		||||
	RepoAdminChangeTeamAccess *bool  `json:"repo_admin_change_team_access"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,11 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/migrations/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/queue"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/modules/secret"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	jsoniter "github.com/json-iterator/go"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -65,6 +68,24 @@ func MigrateRepository(doer, u *models.User, opts base.MigrateOptions) error {
 | 
			
		||||
 | 
			
		||||
// CreateMigrateTask creates a migrate task
 | 
			
		||||
func CreateMigrateTask(doer, u *models.User, opts base.MigrateOptions) (*models.Task, error) {
 | 
			
		||||
	// encrypt credentials for persistence
 | 
			
		||||
	var err error
 | 
			
		||||
	opts.CloneAddrEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.CloneAddr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	opts.CloneAddr = util.SanitizeURLCredentials(opts.CloneAddr, true)
 | 
			
		||||
	opts.AuthPasswordEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.AuthPassword)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	opts.AuthPassword = ""
 | 
			
		||||
	opts.AuthTokenEncrypted, err = secret.EncryptSecret(setting.SecretKey, opts.AuthToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	opts.AuthToken = ""
 | 
			
		||||
 | 
			
		||||
	json := jsoniter.ConfigCompatibleWithStandardLibrary
 | 
			
		||||
	bs, err := json.Marshal(&opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,7 @@ func SetCookie(resp http.ResponseWriter, name string, value string, others ...in
 | 
			
		||||
	if len(others) > 2 {
 | 
			
		||||
		if v, ok := others[2].(string); ok && len(v) > 0 {
 | 
			
		||||
			cookie.Domain = v
 | 
			
		||||
		} else if v, ok := others[1].(func(*http.Cookie)); ok {
 | 
			
		||||
		} else if v, ok := others[2].(func(*http.Cookie)); ok {
 | 
			
		||||
			v(&cookie)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -170,7 +170,7 @@ func SetCookie(resp http.ResponseWriter, name string, value string, others ...in
 | 
			
		||||
	if len(others) > 4 {
 | 
			
		||||
		if v, ok := others[4].(bool); ok && v {
 | 
			
		||||
			cookie.HttpOnly = true
 | 
			
		||||
		} else if v, ok := others[1].(func(*http.Cookie)); ok {
 | 
			
		||||
		} else if v, ok := others[4].(func(*http.Cookie)); ok {
 | 
			
		||||
			v(&cookie)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -179,7 +179,7 @@ func SetCookie(resp http.ResponseWriter, name string, value string, others ...in
 | 
			
		||||
		if v, ok := others[5].(time.Time); ok {
 | 
			
		||||
			cookie.Expires = v
 | 
			
		||||
			cookie.RawExpires = v.Format(time.UnixDate)
 | 
			
		||||
		} else if v, ok := others[1].(func(*http.Cookie)); ok {
 | 
			
		||||
		} else if v, ok := others[5].(func(*http.Cookie)); ok {
 | 
			
		||||
			v(&cookie)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
package web
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	goctx "context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"reflect"
 | 
			
		||||
@@ -27,6 +28,7 @@ func Wrap(handlers ...interface{}) http.HandlerFunc {
 | 
			
		||||
		switch t := handler.(type) {
 | 
			
		||||
		case http.HandlerFunc, func(http.ResponseWriter, *http.Request),
 | 
			
		||||
			func(ctx *context.Context),
 | 
			
		||||
			func(ctx *context.Context) goctx.CancelFunc,
 | 
			
		||||
			func(*context.APIContext),
 | 
			
		||||
			func(*context.PrivateContext),
 | 
			
		||||
			func(http.Handler) http.Handler:
 | 
			
		||||
@@ -48,6 +50,15 @@ func Wrap(handlers ...interface{}) http.HandlerFunc {
 | 
			
		||||
				if r, ok := resp.(context.ResponseWriter); ok && r.Status() > 0 {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			case func(ctx *context.Context) goctx.CancelFunc:
 | 
			
		||||
				ctx := context.GetContext(req)
 | 
			
		||||
				cancel := t(ctx)
 | 
			
		||||
				if cancel != nil {
 | 
			
		||||
					defer cancel()
 | 
			
		||||
				}
 | 
			
		||||
				if ctx.Written() {
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
			case func(ctx *context.Context):
 | 
			
		||||
				ctx := context.GetContext(req)
 | 
			
		||||
				t(ctx)
 | 
			
		||||
@@ -94,6 +105,23 @@ func Middle(f func(ctx *context.Context)) func(netx http.Handler) http.Handler {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MiddleCancel wrap a context function as a chi middleware
 | 
			
		||||
func MiddleCancel(f func(ctx *context.Context) goctx.CancelFunc) func(netx http.Handler) http.Handler {
 | 
			
		||||
	return func(next http.Handler) http.Handler {
 | 
			
		||||
		return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
 | 
			
		||||
			ctx := context.GetContext(req)
 | 
			
		||||
			cancel := f(ctx)
 | 
			
		||||
			if cancel != nil {
 | 
			
		||||
				defer cancel()
 | 
			
		||||
			}
 | 
			
		||||
			if ctx.Written() {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			next.ServeHTTP(ctx.Resp, ctx.Req)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MiddleAPI wrap a context function as a chi middleware
 | 
			
		||||
func MiddleAPI(f func(ctx *context.APIContext)) func(netx http.Handler) http.Handler {
 | 
			
		||||
	return func(next http.Handler) http.Handler {
 | 
			
		||||
@@ -163,6 +191,8 @@ func (r *Route) Use(middlewares ...interface{}) {
 | 
			
		||||
				r.R.Use(t)
 | 
			
		||||
			case func(*context.Context):
 | 
			
		||||
				r.R.Use(Middle(t))
 | 
			
		||||
			case func(*context.Context) goctx.CancelFunc:
 | 
			
		||||
				r.R.Use(MiddleCancel(t))
 | 
			
		||||
			case func(*context.APIContext):
 | 
			
		||||
				r.R.Use(MiddleAPI(t))
 | 
			
		||||
			default:
 | 
			
		||||
 
 | 
			
		||||
@@ -2281,6 +2281,7 @@ auths.allowed_domains_helper = Leave empty to allow all domains. Separate multip
 | 
			
		||||
auths.enable_tls = Enable TLS Encryption
 | 
			
		||||
auths.skip_tls_verify = Skip TLS Verify
 | 
			
		||||
auths.pam_service_name = PAM Service Name
 | 
			
		||||
auths.pam_email_domain = PAM Email Domain (optional)
 | 
			
		||||
auths.oauth2_provider = OAuth2 Provider
 | 
			
		||||
auths.oauth2_icon_url = Icon URL
 | 
			
		||||
auths.oauth2_clientID = Client ID (Key)
 | 
			
		||||
 
 | 
			
		||||
@@ -239,6 +239,7 @@ func NewAuthSourcePost(ctx *context.Context) {
 | 
			
		||||
	case models.LoginPAM:
 | 
			
		||||
		config = &models.PAMConfig{
 | 
			
		||||
			ServiceName: form.PAMServiceName,
 | 
			
		||||
			EmailDomain: form.PAMEmailDomain,
 | 
			
		||||
		}
 | 
			
		||||
	case models.LoginOAuth2:
 | 
			
		||||
		config = parseOAuth2Config(form)
 | 
			
		||||
@@ -346,6 +347,7 @@ func EditAuthSourcePost(ctx *context.Context) {
 | 
			
		||||
	case models.LoginPAM:
 | 
			
		||||
		config = &models.PAMConfig{
 | 
			
		||||
			ServiceName: form.PAMServiceName,
 | 
			
		||||
			EmailDomain: form.PAMEmailDomain,
 | 
			
		||||
		}
 | 
			
		||||
	case models.LoginOAuth2:
 | 
			
		||||
		config = parseOAuth2Config(form)
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,10 @@ func DeleteRepo(ctx *context.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo != nil && ctx.Repo.GitRepo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repo.ID {
 | 
			
		||||
		ctx.Repo.GitRepo.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
 | 
			
		||||
		ctx.ServerError("DeleteRepository", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -557,6 +557,7 @@ func Routes() *web.Route {
 | 
			
		||||
		Gclifetime:     setting.SessionConfig.Gclifetime,
 | 
			
		||||
		Maxlifetime:    setting.SessionConfig.Maxlifetime,
 | 
			
		||||
		Secure:         setting.SessionConfig.Secure,
 | 
			
		||||
		SameSite:       setting.SessionConfig.SameSite,
 | 
			
		||||
		Domain:         setting.SessionConfig.Domain,
 | 
			
		||||
	}))
 | 
			
		||||
	m.Use(securityHeaders())
 | 
			
		||||
@@ -716,7 +717,7 @@ func Routes() *web.Route {
 | 
			
		||||
			m.Group("/{username}/{reponame}", func() {
 | 
			
		||||
				m.Combo("").Get(reqAnyRepoReader(), repo.Get).
 | 
			
		||||
					Delete(reqToken(), reqOwner(), repo.Delete).
 | 
			
		||||
					Patch(reqToken(), reqAdmin(), context.RepoRefForAPI, bind(api.EditRepoOption{}), repo.Edit)
 | 
			
		||||
					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
 | 
			
		||||
				m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
 | 
			
		||||
				m.Combo("/notifications").
 | 
			
		||||
					Get(reqToken(), notify.ListRepoNotifications).
 | 
			
		||||
@@ -892,7 +893,7 @@ func Routes() *web.Route {
 | 
			
		||||
						Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
 | 
			
		||||
					m.Group("/{index}", func() {
 | 
			
		||||
						m.Combo("").Get(repo.GetPullRequest).
 | 
			
		||||
							Patch(reqToken(), reqRepoWriter(models.UnitTypePullRequests), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
 | 
			
		||||
							Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
 | 
			
		||||
						m.Get(".diff", repo.DownloadPullDiff)
 | 
			
		||||
						m.Get(".patch", repo.DownloadPullPatch)
 | 
			
		||||
						m.Post("/update", reqToken(), repo.UpdatePullRequest)
 | 
			
		||||
 
 | 
			
		||||
@@ -264,7 +264,13 @@ func Edit(ctx *context.APIContext) {
 | 
			
		||||
	if form.Visibility != "" {
 | 
			
		||||
		org.Visibility = api.VisibilityModes[form.Visibility]
 | 
			
		||||
	}
 | 
			
		||||
	if err := models.UpdateUserCols(org, "full_name", "description", "website", "location", "visibility"); err != nil {
 | 
			
		||||
	if form.RepoAdminChangeTeamAccess != nil {
 | 
			
		||||
		org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess
 | 
			
		||||
	}
 | 
			
		||||
	if err := models.UpdateUserCols(org,
 | 
			
		||||
		"full_name", "description", "website", "location",
 | 
			
		||||
		"visibility", "repo_admin_change_team_access",
 | 
			
		||||
	); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "EditOrganization", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
@@ -13,7 +14,6 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/convert"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
@@ -117,62 +117,20 @@ func DeleteBranch(ctx *context.APIContext) {
 | 
			
		||||
 | 
			
		||||
	branchName := ctx.Params("*")
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Repository.DefaultBranch == branchName {
 | 
			
		||||
		ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	isProtected, err := ctx.Repo.Repository.IsProtectedBranch(branchName, ctx.User)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.InternalServerError(err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if isProtected {
 | 
			
		||||
		ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	branch, err := repo_module.GetBranch(ctx.Repo.Repository, branchName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if git.IsErrBranchNotExist(err) {
 | 
			
		||||
	if err := repo_service.DeleteBranch(ctx.User, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
 | 
			
		||||
		switch {
 | 
			
		||||
		case git.IsErrBranchNotExist(err):
 | 
			
		||||
			ctx.NotFound(err)
 | 
			
		||||
		} else {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "GetBranch", err)
 | 
			
		||||
		case errors.Is(err, repo_service.ErrBranchIsDefault):
 | 
			
		||||
			ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
 | 
			
		||||
		case errors.Is(err, repo_service.ErrBranchIsProtected):
 | 
			
		||||
			ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
 | 
			
		||||
		default:
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c, err := branch.GetCommit()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "GetCommit", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ctx.Repo.GitRepo.DeleteBranch(branchName, git.DeleteBranchOptions{
 | 
			
		||||
		Force: true,
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Don't return error below this
 | 
			
		||||
	if err := repo_service.PushUpdate(
 | 
			
		||||
		&repo_module.PushUpdateOptions{
 | 
			
		||||
			RefFullName:  git.BranchPrefix + branchName,
 | 
			
		||||
			OldCommitID:  c.ID.String(),
 | 
			
		||||
			NewCommitID:  git.EmptySHA,
 | 
			
		||||
			PusherID:     ctx.User.ID,
 | 
			
		||||
			PusherName:   ctx.User.Name,
 | 
			
		||||
			RepoUserName: ctx.Repo.Owner.Name,
 | 
			
		||||
			RepoName:     ctx.Repo.Repository.Name,
 | 
			
		||||
		}); err != nil {
 | 
			
		||||
		log.Error("Update: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ctx.Repo.Repository.AddDeletedBranch(branchName, c.ID.String(), ctx.User.ID); err != nil {
 | 
			
		||||
		log.Warn("AddDeletedBranch: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Status(http.StatusNoContent)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -202,8 +202,8 @@ func CreateRelease(ctx *context.APIContext) {
 | 
			
		||||
		rel.Repo = ctx.Repo.Repository
 | 
			
		||||
		rel.Publisher = ctx.User
 | 
			
		||||
 | 
			
		||||
		if err = releaseservice.UpdateReleaseOrCreatReleaseFromTag(ctx.User, ctx.Repo.GitRepo, rel, nil, true); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "UpdateReleaseOrCreatReleaseFromTag", err)
 | 
			
		||||
		if err = releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
 | 
			
		||||
			ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@@ -277,8 +277,8 @@ func EditRelease(ctx *context.APIContext) {
 | 
			
		||||
	if form.IsPrerelease != nil {
 | 
			
		||||
		rel.IsPrerelease = *form.IsPrerelease
 | 
			
		||||
	}
 | 
			
		||||
	if err := releaseservice.UpdateReleaseOrCreatReleaseFromTag(ctx.User, ctx.Repo.GitRepo, rel, nil, false); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "UpdateReleaseOrCreatReleaseFromTag", err)
 | 
			
		||||
	if err := releaseservice.UpdateRelease(ctx.User, ctx.Repo.GitRepo, rel, nil, nil, nil); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "UpdateRelease", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -578,7 +578,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
 | 
			
		||||
		repo.IsTemplate = *opts.Template
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.GitRepo == nil {
 | 
			
		||||
	if ctx.Repo.GitRepo == nil && !repo.IsEmpty {
 | 
			
		||||
		var err error
 | 
			
		||||
		ctx.Repo.GitRepo, err = git.OpenRepository(ctx.Repo.Repository.RepoPath())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -589,13 +589,13 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Default branch only updated if changed and exist or the repository is empty
 | 
			
		||||
	if opts.DefaultBranch != nil &&
 | 
			
		||||
		repo.DefaultBranch != *opts.DefaultBranch &&
 | 
			
		||||
		(ctx.Repo.Repository.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
 | 
			
		||||
		if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil {
 | 
			
		||||
			if !git.IsErrUnsupportedVersion(err) {
 | 
			
		||||
				ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
 | 
			
		||||
				return err
 | 
			
		||||
	if opts.DefaultBranch != nil && repo.DefaultBranch != *opts.DefaultBranch && (repo.IsEmpty || ctx.Repo.GitRepo.IsBranchExist(*opts.DefaultBranch)) {
 | 
			
		||||
		if !repo.IsEmpty {
 | 
			
		||||
			if err := ctx.Repo.GitRepo.SetDefaultBranch(*opts.DefaultBranch); err != nil {
 | 
			
		||||
				if !git.IsErrUnsupportedVersion(err) {
 | 
			
		||||
					ctx.Error(http.StatusInternalServerError, "SetDefaultBranch", err)
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		repo.DefaultBranch = *opts.DefaultBranch
 | 
			
		||||
@@ -885,6 +885,10 @@ func Delete(ctx *context.APIContext) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.GitRepo != nil {
 | 
			
		||||
		ctx.Repo.GitRepo.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, "DeleteRepository", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ func parseTime(value string) (int64, error) {
 | 
			
		||||
// prepareQueryArg unescape and trim a query arg
 | 
			
		||||
func prepareQueryArg(ctx *context.APIContext, name string) (value string, err error) {
 | 
			
		||||
	value, err = url.PathUnescape(ctx.Query(name))
 | 
			
		||||
	value = strings.Trim(value, " ")
 | 
			
		||||
	value = strings.TrimSpace(value)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
@@ -61,6 +62,8 @@ func InstallInit(next http.Handler) http.Handler {
 | 
			
		||||
				"DbOptions":     setting.SupportedDatabases,
 | 
			
		||||
				"i18n":          locale,
 | 
			
		||||
				"Language":      locale.Language(),
 | 
			
		||||
				"Lang":          locale.Language(),
 | 
			
		||||
				"AllLangs":      translation.AllLangs(),
 | 
			
		||||
				"CurrentURL":    setting.AppSubURL + req.URL.RequestURI(),
 | 
			
		||||
				"PageStartTime": startTime,
 | 
			
		||||
				"TmplLoadTimes": func() string {
 | 
			
		||||
@@ -69,6 +72,12 @@ func InstallInit(next http.Handler) http.Handler {
 | 
			
		||||
				"PasswordHashAlgorithms": models.AvailableHashAlgorithms,
 | 
			
		||||
			},
 | 
			
		||||
		}
 | 
			
		||||
		for _, lang := range translation.AllLangs() {
 | 
			
		||||
			if lang.Lang == locale.Language() {
 | 
			
		||||
				ctx.Data["LangName"] = lang.Name
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Req = context.WithContext(req, &ctx)
 | 
			
		||||
		next.ServeHTTP(resp, ctx.Req)
 | 
			
		||||
	})
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,7 @@ func SettingsPost(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	org := ctx.Org.Organization
 | 
			
		||||
	nameChanged := org.Name != form.Name
 | 
			
		||||
 | 
			
		||||
	// Check if organization name has been changed.
 | 
			
		||||
	if org.LowerName != strings.ToLower(form.Name) {
 | 
			
		||||
@@ -74,7 +75,9 @@ func SettingsPost(ctx *context.Context) {
 | 
			
		||||
		// reset ctx.org.OrgLink with new name
 | 
			
		||||
		ctx.Org.OrgLink = setting.AppSubURL + "/org/" + form.Name
 | 
			
		||||
		log.Trace("Organization name changed: %s -> %s", org.Name, form.Name)
 | 
			
		||||
		nameChanged = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// In case it's just a case change.
 | 
			
		||||
	org.Name = form.Name
 | 
			
		||||
	org.LowerName = strings.ToLower(form.Name)
 | 
			
		||||
@@ -104,11 +107,17 @@ func SettingsPost(ctx *context.Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		for _, repo := range org.Repos {
 | 
			
		||||
			repo.OwnerName = org.Name
 | 
			
		||||
			if err := models.UpdateRepository(repo, true); err != nil {
 | 
			
		||||
				ctx.ServerError("UpdateRepository", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else if nameChanged {
 | 
			
		||||
		if err := models.UpdateRepositoryOwnerNames(org.ID, org.Name); err != nil {
 | 
			
		||||
			ctx.ServerError("UpdateRepository", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("Organization setting updated: %s", org.Name)
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,7 @@ func Routes() *web.Route {
 | 
			
		||||
	r.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger)
 | 
			
		||||
	r.Post("/manager/remove-logger/{group}/{name}", RemoveLogger)
 | 
			
		||||
	r.Post("/mail/send", SendEmail)
 | 
			
		||||
	r.Post("/restore_repo", RestoreRepo)
 | 
			
		||||
 | 
			
		||||
	return r
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user