mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 08:02:36 +09:00 
			
		
		
		
	Compare commits
	
		
			51 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					136ec9ef81 | ||
| 
						 | 
					79018ae726 | ||
| 
						 | 
					e11176192a | ||
| 
						 | 
					4e0269e890 | ||
| 
						 | 
					04114c637a | ||
| 
						 | 
					e5540bfa81 | ||
| 
						 | 
					d22d6ca0d8 | ||
| 
						 | 
					d49feab428 | ||
| 
						 | 
					9162f4403a | ||
| 
						 | 
					d05cf08fad | ||
| 
						 | 
					f4b4b0bf98 | ||
| 
						 | 
					99596044d7 | ||
| 
						 | 
					693d26914f | ||
| 
						 | 
					315f197790 | ||
| 
						 | 
					76b8f0c3a7 | ||
| 
						 | 
					f99bbd7f3f | ||
| 
						 | 
					f7ef657b5a | ||
| 
						 | 
					486d274be6 | ||
| 
						 | 
					ab3d2a944c | ||
| 
						 | 
					12bfa9e83d | ||
| 
						 | 
					dd661e92df | ||
| 
						 | 
					0b31272c7e | ||
| 
						 | 
					ec0c418719 | ||
| 
						 | 
					6dc19fc29a | ||
| 
						 | 
					9f1baa7d18 | ||
| 
						 | 
					e13deb7a16 | ||
| 
						 | 
					e5c1b8b632 | ||
| 
						 | 
					e931b62f33 | ||
| 
						 | 
					81ee93e5bc | ||
| 
						 | 
					053f9186bc | ||
| 
						 | 
					68fcdb6122 | ||
| 
						 | 
					14ca309c39 | ||
| 
						 | 
					4aba42519d | ||
| 
						 | 
					9adf175df0 | ||
| 
						 | 
					c3fa2a8729 | ||
| 
						 | 
					89dfed32e0 | ||
| 
						 | 
					d5062d0c27 | ||
| 
						 | 
					90e9e79232 | ||
| 
						 | 
					c6467edcb1 | ||
| 
						 | 
					5d5b695527 | ||
| 
						 | 
					0af7a7b79f | ||
| 
						 | 
					9339661078 | ||
| 
						 | 
					1e69f085d6 | ||
| 
						 | 
					0bfccd8ecf | ||
| 
						 | 
					534b9b35dd | ||
| 
						 | 
					dbadc59b56 | ||
| 
						 | 
					a57e2c4bc3 | ||
| 
						 | 
					acd4e10990 | ||
| 
						 | 
					0a1df294c8 | ||
| 
						 | 
					52a964d1fc | ||
| 
						 | 
					d3dbe0d9ce | 
@@ -7,7 +7,7 @@
 | 
			
		||||
      "version": "20"
 | 
			
		||||
    },
 | 
			
		||||
    "ghcr.io/devcontainers/features/git-lfs:1.2.2": {},
 | 
			
		||||
    "ghcr.io/devcontainers-contrib/features/poetry:2": {},
 | 
			
		||||
    "ghcr.io/devcontainers-extra/features/poetry:2": {},
 | 
			
		||||
    "ghcr.io/devcontainers/features/python:1": {
 | 
			
		||||
      "version": "3.12"
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										79
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -4,7 +4,68 @@ This changelog goes through 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.com).
 | 
			
		||||
 | 
			
		||||
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/1.24.1) - 2025-06-18
 | 
			
		||||
## [1.24.5](https://github.com/go-gitea/gitea/releases/tag/v1.24.5) - 2025-08-12
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix a bug where lfs gc never worked. (#35198) (#35255)
 | 
			
		||||
  * Reload issue when sending webhook to make num comments is right. (#35243) (#35248)
 | 
			
		||||
  * Fix bug when review pull request commits (#35192) (#35246)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Vertically center "Show Resolved" (#35211) (#35218)
 | 
			
		||||
 | 
			
		||||
## [1.24.4](https://github.com/go-gitea/gitea/releases/tag/v1.24.4) - 2025-08-03
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix various bugs (1.24) (#35186)
 | 
			
		||||
  * Fix migrate input box bug (#35166) (#35171)
 | 
			
		||||
  * Only hide dropzone when no files have been uploaded (#35156) (#35167)
 | 
			
		||||
  * Fix review comment/dimiss comment x reference can be refereced back (#35094) (#35099)
 | 
			
		||||
  * Fix submodule nil check (#35096) (#35098)
 | 
			
		||||
* MISC
 | 
			
		||||
  * Don't use full-file highlight when there is a git diff textconv (#35114) (#35119)
 | 
			
		||||
  * Increase gap on latest commit (#35104) (#35113)
 | 
			
		||||
 | 
			
		||||
## [1.24.3](https://github.com/go-gitea/gitea/releases/tag/v1.24.3) - 2025-07-15
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix form property assignment edge case (#35073) (#35078)
 | 
			
		||||
  * Improve submodule relative path handling (#35056) (#35075)
 | 
			
		||||
  * Fix incorrect comment diff hunk parsing, fix github asset ID nil panic (#35046) (#35055)
 | 
			
		||||
  * Fix updating user visibility (#35036) (#35044)
 | 
			
		||||
  * Support base64-encoded agit push options (#35037) (#35041)
 | 
			
		||||
  * Make submodule link work with relative path (#35034) (#35038)
 | 
			
		||||
  * Fix bug when displaying git user avatar in commits list (#35006)
 | 
			
		||||
  * Fix API response for swagger spec (#35029)
 | 
			
		||||
  * Start automerge check again after the conflict check and the schedule (#34988) (#35002)
 | 
			
		||||
  * Fix the response format for actions/workflows (#35009) (#35016)
 | 
			
		||||
  * Fix repo settings and protocol log problems (#35012) (#35013)
 | 
			
		||||
  * Fix project images scroll (#34971) (#34972)
 | 
			
		||||
  * Mark old reviews as stale on agit pr updates (#34933) (#34965)
 | 
			
		||||
  * Fix git graph page (#34948) (#34949)
 | 
			
		||||
  * Don't send trigger for a pending review's comment create/update/delete (#34928) (#34939)
 | 
			
		||||
  * Fix some log and UI problems (#34863) (#34868)
 | 
			
		||||
  * Fix archive API (#34853) (#34857)
 | 
			
		||||
  * Ignore force pushes for changed files in a PR review (#34837) (#34843)
 | 
			
		||||
  * Fix SSH LFS timeout (#34838) (#34842)
 | 
			
		||||
  * Fix team permissions (#34827) (#34836)
 | 
			
		||||
  * Fix job status aggregation logic (#34823) (#34835)
 | 
			
		||||
  * Fix issue filter (#34914) (#34915)
 | 
			
		||||
  * Fix typo in pull request merge warning message text (#34899) (#34903)
 | 
			
		||||
  * Support the open-icon of folder (#34168) (#34896)
 | 
			
		||||
  * Optimize flex layout of release attachment area (#34885) (#34886)
 | 
			
		||||
  * Fix the issue of abnormal interface when there is no issue-item on the project page (#34791) (#34880)
 | 
			
		||||
  * Skip updating timestamp when sync branch (#34875)
 | 
			
		||||
  * Fix required contexts and commit status matching bug (#34815) (#34829)
 | 
			
		||||
 | 
			
		||||
## [1.24.2](https://github.com/go-gitea/gitea/releases/tag/v1.24.2) - 2025-06-20
 | 
			
		||||
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix container range bug (#34795) (#34796)
 | 
			
		||||
  * Upgrade chi to v5.2.2 (#34798) (#34799)
 | 
			
		||||
* BUILD
 | 
			
		||||
  * Bump poetry feature to new url for dev container (#34787) (#34790)
 | 
			
		||||
 | 
			
		||||
## [1.24.1](https://github.com/go-gitea/gitea/releases/tag/v1.24.1) - 2025-06-18
 | 
			
		||||
 | 
			
		||||
* ENHANCEMENTS
 | 
			
		||||
  * Improve alignment of commit status icon on commit page (#34750) (#34757)
 | 
			
		||||
@@ -24,7 +85,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
  * Hide href attribute of a tag if there is no target_url (#34556) (#34684)
 | 
			
		||||
  * Fix tag target (#34781) #34783
 | 
			
		||||
 | 
			
		||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26
 | 
			
		||||
## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/v1.24.0) - 2025-05-26
 | 
			
		||||
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076)
 | 
			
		||||
@@ -394,7 +455,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
  * Bump x/net (#32896) (#32900)
 | 
			
		||||
  * Only activity tab needs heatmap data loading (#34652)
 | 
			
		||||
 | 
			
		||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11
 | 
			
		||||
## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/v1.23.8) - 2025-05-11
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Fix a bug when uploading file via lfs ssh command (#34408) (#34411)
 | 
			
		||||
@@ -421,7 +482,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
  * Bump go version in go.mod (#34160)
 | 
			
		||||
  * remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158)
 | 
			
		||||
 | 
			
		||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07
 | 
			
		||||
## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/v1.23.7) - 2025-04-07
 | 
			
		||||
 | 
			
		||||
* Enhancements
 | 
			
		||||
  * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071)
 | 
			
		||||
@@ -519,7 +580,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com).
 | 
			
		||||
* BUGFIXES
 | 
			
		||||
  * Fix a bug caused by status webhook template #33512
 | 
			
		||||
 | 
			
		||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/1.23.2) - 2025-02-04
 | 
			
		||||
## [1.23.2](https://github.com/go-gitea/gitea/releases/tag/v1.23.2) - 2025-02-04
 | 
			
		||||
 | 
			
		||||
* BREAKING
 | 
			
		||||
  * Add tests for webhook and fix some webhook bugs (#33396) (#33442)
 | 
			
		||||
@@ -3049,7 +3110,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Improve decryption failure message (#24573) (#24575)
 | 
			
		||||
  * Makefile: Use portable !, not GNUish -not, with find(1). (#24565) (#24572)
 | 
			
		||||
 | 
			
		||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/1.19.3) - 2023-05-03
 | 
			
		||||
## [1.19.3](https://github.com/go-gitea/gitea/releases/tag/v1.19.3) - 2023-05-03
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Use golang 1.20.4 to fix CVE-2023-24539, CVE-2023-24540, and CVE-2023-29400
 | 
			
		||||
@@ -3062,7 +3123,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Fix incorrect CurrentUser check for docker rootless (#24435)
 | 
			
		||||
  * Getting the tag list does not require being signed in (#24413) (#24416)
 | 
			
		||||
 | 
			
		||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/1.19.2) - 2023-04-26
 | 
			
		||||
## [1.19.2](https://github.com/go-gitea/gitea/releases/tag/v1.19.2) - 2023-04-26
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Require repo scope for PATs for private repos and basic authentication (#24362) (#24364)
 | 
			
		||||
@@ -3561,7 +3622,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Display attachments of review comment when comment content is blank (#23035) (#23046)
 | 
			
		||||
  * Return empty url for submodule tree entries (#23043) (#23048)
 | 
			
		||||
 | 
			
		||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/1.18.4) - 2023-02-20
 | 
			
		||||
## [1.18.4](https://github.com/go-gitea/gitea/releases/tag/v1.18.4) - 2023-02-20
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Provide the ability to set password hash algorithm parameters (#22942) (#22943)
 | 
			
		||||
@@ -3988,7 +4049,7 @@ Key highlights of this release encompass significant changes categorized under `
 | 
			
		||||
  * Fix the mode of custom dir to 0700 in docker-rootless (#20861) (#20867)
 | 
			
		||||
  * Fix UI mis-align for PR commit history (#20845) (#20859)
 | 
			
		||||
 | 
			
		||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/1.17.1) - 2022-08-17
 | 
			
		||||
## [1.17.1](https://github.com/go-gitea/gitea/releases/tag/v1.17.1) - 2022-08-17
 | 
			
		||||
 | 
			
		||||
* SECURITY
 | 
			
		||||
  * Correctly escape within tribute.js (#20831) (#20832)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								Makefile
									
									
									
									
									
								
							@@ -47,6 +47,17 @@ ifeq ($(HAS_GO), yes)
 | 
			
		||||
	CGO_CFLAGS ?= $(shell $(GO) env CGO_CFLAGS) $(CGO_EXTRA_CFLAGS)
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
CGO_ENABLED ?= 0
 | 
			
		||||
ifneq (,$(findstring sqlite,$(TAGS))$(findstring pam,$(TAGS)))
 | 
			
		||||
	CGO_ENABLED = 1
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
STATIC ?=
 | 
			
		||||
EXTLDFLAGS ?=
 | 
			
		||||
ifneq ($(STATIC),)
 | 
			
		||||
	EXTLDFLAGS = -extldflags "-static"
 | 
			
		||||
endif
 | 
			
		||||
 | 
			
		||||
ifeq ($(GOOS),windows)
 | 
			
		||||
	IS_WINDOWS := yes
 | 
			
		||||
else ifeq ($(patsubst Windows%,Windows,$(OS)),Windows)
 | 
			
		||||
@@ -740,7 +751,10 @@ security-check:
 | 
			
		||||
	go run $(GOVULNCHECK_PACKAGE) -show color ./...
 | 
			
		||||
 | 
			
		||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
 | 
			
		||||
	CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(LDFLAGS)' -o $@
 | 
			
		||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
 | 
			
		||||
  $(error pam support set via TAGS doesn't support static builds)
 | 
			
		||||
endif
 | 
			
		||||
	CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
 | 
			
		||||
 | 
			
		||||
.PHONY: release
 | 
			
		||||
release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								flake.lock
									
									
									
										generated
									
									
									
								
							@@ -20,11 +20,11 @@
 | 
			
		||||
    },
 | 
			
		||||
    "nixpkgs": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "lastModified": 1747179050,
 | 
			
		||||
        "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
 | 
			
		||||
        "lastModified": 1752480373,
 | 
			
		||||
        "narHash": "sha256-JHQbm+OcGp32wAsXTE/FLYGNpb+4GLi5oTvCxwSoBOA=",
 | 
			
		||||
        "owner": "nixos",
 | 
			
		||||
        "repo": "nixpkgs",
 | 
			
		||||
        "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
 | 
			
		||||
        "rev": "62e0f05ede1da0d54515d4ea8ce9c733f12d9f08",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										60
									
								
								flake.nix
									
									
									
									
									
								
							@@ -11,33 +11,45 @@
 | 
			
		||||
        pkgs = nixpkgs.legacyPackages.${system};
 | 
			
		||||
      in
 | 
			
		||||
      {
 | 
			
		||||
        devShells.default = pkgs.mkShell {
 | 
			
		||||
          buildInputs = with pkgs; [
 | 
			
		||||
            # generic
 | 
			
		||||
            git
 | 
			
		||||
            git-lfs
 | 
			
		||||
            gnumake
 | 
			
		||||
            gnused
 | 
			
		||||
            gnutar
 | 
			
		||||
            gzip
 | 
			
		||||
        devShells.default =
 | 
			
		||||
          with pkgs;
 | 
			
		||||
          let
 | 
			
		||||
            # only bump toolchain versions here
 | 
			
		||||
            go = go_1_24;
 | 
			
		||||
            nodejs = nodejs_24;
 | 
			
		||||
            python3 = python312;
 | 
			
		||||
          in
 | 
			
		||||
          pkgs.mkShell {
 | 
			
		||||
            buildInputs = [
 | 
			
		||||
              # generic
 | 
			
		||||
              git
 | 
			
		||||
              git-lfs
 | 
			
		||||
              gnumake
 | 
			
		||||
              gnused
 | 
			
		||||
              gnutar
 | 
			
		||||
              gzip
 | 
			
		||||
 | 
			
		||||
            # frontend
 | 
			
		||||
            nodejs_22
 | 
			
		||||
              # frontend
 | 
			
		||||
              nodejs
 | 
			
		||||
 | 
			
		||||
            # linting
 | 
			
		||||
            python312
 | 
			
		||||
            poetry
 | 
			
		||||
              # linting
 | 
			
		||||
              python3
 | 
			
		||||
              poetry
 | 
			
		||||
 | 
			
		||||
            # backend
 | 
			
		||||
            go_1_24
 | 
			
		||||
            gofumpt
 | 
			
		||||
            sqlite
 | 
			
		||||
          ];
 | 
			
		||||
          shellHook = ''
 | 
			
		||||
            export GO="${pkgs.go_1_24}/bin/go"
 | 
			
		||||
            export GOROOT="${pkgs.go_1_24}/share/go"
 | 
			
		||||
          '';
 | 
			
		||||
        };
 | 
			
		||||
              # backend
 | 
			
		||||
              go
 | 
			
		||||
              glibc.static
 | 
			
		||||
              gofumpt
 | 
			
		||||
              sqlite
 | 
			
		||||
            ];
 | 
			
		||||
            CFLAGS = "-I${glibc.static.dev}/include";
 | 
			
		||||
            LDFLAGS = "-L ${glibc.static}/lib";
 | 
			
		||||
            GO = "${go}/bin/go";
 | 
			
		||||
            GOROOT = "${go}/share/go";
 | 
			
		||||
 | 
			
		||||
            TAGS = "sqlite sqlite_unlock_notify";
 | 
			
		||||
            STATIC = "true";
 | 
			
		||||
          };
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@@ -51,7 +51,7 @@ require (
 | 
			
		||||
	github.com/gliderlabs/ssh v0.3.8
 | 
			
		||||
	github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d
 | 
			
		||||
	github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
 | 
			
		||||
	github.com/go-chi/chi/v5 v5.2.1
 | 
			
		||||
	github.com/go-chi/chi/v5 v5.2.2
 | 
			
		||||
	github.com/go-chi/cors v1.2.1
 | 
			
		||||
	github.com/go-co-op/gocron v1.37.0
 | 
			
		||||
	github.com/go-enry/go-enry/v2 v2.9.2
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@@ -301,8 +301,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
 | 
			
		||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
 | 
			
		||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
 | 
			
		||||
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 | 
			
		||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
 | 
			
		||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
 | 
			
		||||
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
 | 
			
		||||
 
 | 
			
		||||
@@ -185,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status {
 | 
			
		||||
		return StatusSuccess
 | 
			
		||||
	case hasCancelled:
 | 
			
		||||
		return StatusCancelled
 | 
			
		||||
	case hasFailure:
 | 
			
		||||
		return StatusFailure
 | 
			
		||||
	case hasRunning:
 | 
			
		||||
		return StatusRunning
 | 
			
		||||
	case hasFailure:
 | 
			
		||||
		return StatusFailure
 | 
			
		||||
	case hasWaiting:
 | 
			
		||||
		return StatusWaiting
 | 
			
		||||
	case hasBlocked:
 | 
			
		||||
 
 | 
			
		||||
@@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) {
 | 
			
		||||
		{[]Status{StatusCancelled, StatusRunning}, StatusCancelled},
 | 
			
		||||
		{[]Status{StatusCancelled, StatusBlocked}, StatusCancelled},
 | 
			
		||||
 | 
			
		||||
		// failure with other status, fail fast
 | 
			
		||||
		// Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast.
 | 
			
		||||
		// failure with other status, usually fail fast, but "running" wins to match GitHub's behavior
 | 
			
		||||
		// another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail.
 | 
			
		||||
		{[]Status{StatusFailure}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusSuccess}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusSkipped}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusCancelled}, StatusCancelled},
 | 
			
		||||
		{[]Status{StatusFailure, StatusWaiting}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusRunning}, StatusFailure},
 | 
			
		||||
		{[]Status{StatusFailure, StatusRunning}, StatusRunning},
 | 
			
		||||
		{[]Status{StatusFailure, StatusBlocked}, StatusFailure},
 | 
			
		||||
 | 
			
		||||
		// skipped with other status
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
 | 
			
		||||
			signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
 | 
			
		||||
		}
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("Unable to validate token signature. Error: %v", err)
 | 
			
		||||
			log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err)
 | 
			
		||||
			return nil, ErrGPGInvalidTokenSignature{
 | 
			
		||||
				ID:      ekeys[0].PrimaryKey.KeyIdString(),
 | 
			
		||||
				Wrapped: err,
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if signer == nil {
 | 
			
		||||
		log.Error("Unable to validate token signature. Error: %v", err)
 | 
			
		||||
		log.Debug("VerifyGPGKey failed: no signer")
 | 
			
		||||
		return "", ErrGPGInvalidTokenSignature{
 | 
			
		||||
			ID: key.KeyID,
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
 | 
			
		||||
		// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
 | 
			
		||||
		// see https://github.com/PowerShell/PowerShell/issues/5974
 | 
			
		||||
		if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil {
 | 
			
		||||
			log.Error("Unable to validate token signature. Error: %v", err)
 | 
			
		||||
			log.Debug("VerifySSHKey sshsig.Verify failed: %v", err)
 | 
			
		||||
			return "", ErrSSHInvalidTokenSignature{
 | 
			
		||||
				Fingerprint: key.Fingerprint,
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre
 | 
			
		||||
		return currentWhitelist, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -719,7 +719,8 @@ func (c *Comment) LoadReactions(ctx context.Context, repo *repo_model.Repository
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Comment) loadReview(ctx context.Context) (err error) {
 | 
			
		||||
// LoadReview loads the associated review
 | 
			
		||||
func (c *Comment) LoadReview(ctx context.Context) (err error) {
 | 
			
		||||
	if c.ReviewID == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -736,11 +737,6 @@ func (c *Comment) loadReview(ctx context.Context) (err error) {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadReview loads the associated review
 | 
			
		||||
func (c *Comment) LoadReview(ctx context.Context) error {
 | 
			
		||||
	return c.loadReview(ctx)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
 | 
			
		||||
func (c *Comment) DiffSide() string {
 | 
			
		||||
	if c.Line < 0 {
 | 
			
		||||
@@ -860,7 +856,7 @@ func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment
 | 
			
		||||
		}
 | 
			
		||||
		if comment.ReviewID != 0 {
 | 
			
		||||
			if comment.Review == nil {
 | 
			
		||||
				if err := comment.loadReview(ctx); err != nil {
 | 
			
		||||
				if err := comment.LoadReview(ctx); err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 
 | 
			
		||||
@@ -235,7 +235,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
 | 
			
		||||
 | 
			
		||||
// AddCrossReferences add cross references
 | 
			
		||||
func (c *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
 | 
			
		||||
	if c.Type != CommentTypeCode && c.Type != CommentTypeComment {
 | 
			
		||||
	if !c.Type.HasContentSupport() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if err := c.LoadIssue(stdCtx); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -602,8 +602,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder {
 | 
			
		||||
			"team_user.uid":    userID,
 | 
			
		||||
		})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TeamsWithAccessToRepo returns all teams that have given access level to the repository.
 | 
			
		||||
func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) {
 | 
			
		||||
	return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,8 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TeamRepo represents an team-repository relation.
 | 
			
		||||
@@ -48,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error {
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository.
 | 
			
		||||
func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) {
 | 
			
		||||
// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit.
 | 
			
		||||
// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control.
 | 
			
		||||
// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details
 | 
			
		||||
func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) {
 | 
			
		||||
	teams := make([]*Team, 0, 5)
 | 
			
		||||
	return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode).
 | 
			
		||||
		Join("INNER", "team_repo", "team_repo.team_id = team.id").
 | 
			
		||||
		And("team_repo.org_id = ?", orgID).
 | 
			
		||||
		And("team_repo.repo_id = ?", repoID).
 | 
			
		||||
		OrderBy("name").
 | 
			
		||||
		Find(&teams)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit.
 | 
			
		||||
func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) {
 | 
			
		||||
	teams := make([]*Team, 0, 5)
 | 
			
		||||
	return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode).
 | 
			
		||||
	sub := builder.Select("team_id").From("team_unit").
 | 
			
		||||
		Where(builder.Expr("team_unit.team_id = team.id")).
 | 
			
		||||
		And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))).
 | 
			
		||||
		And(builder.Expr("team_unit.access_mode >= ?", mode))
 | 
			
		||||
 | 
			
		||||
	err := db.GetEngine(ctx).
 | 
			
		||||
		Join("INNER", "team_repo", "team_repo.team_id = team.id").
 | 
			
		||||
		Join("INNER", "team_unit", "team_unit.team_id = team.id").
 | 
			
		||||
		And("team_repo.org_id = ?", orgID).
 | 
			
		||||
		And("team_repo.repo_id = ?", repoID).
 | 
			
		||||
		And("team_unit.type = ?", unitType).
 | 
			
		||||
		And(builder.Or(
 | 
			
		||||
			builder.Expr("team.authorize >= ?", mode),
 | 
			
		||||
			builder.In("team.id", sub),
 | 
			
		||||
		)).
 | 
			
		||||
		OrderBy("name").
 | 
			
		||||
		Find(&teams)
 | 
			
		||||
 | 
			
		||||
	return teams, err
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) {
 | 
			
		||||
	org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41})
 | 
			
		||||
	repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61})
 | 
			
		||||
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
	teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	if assert.Len(t, teams, 2) {
 | 
			
		||||
		assert.EqualValues(t, 21, teams[0].ID)
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ func (p *Permission) IsAdmin() bool {
 | 
			
		||||
 | 
			
		||||
// HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository.
 | 
			
		||||
// It doesn't count the "public(anonymous/everyone) access mode".
 | 
			
		||||
// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess`
 | 
			
		||||
func (p *Permission) HasAnyUnitAccess() bool {
 | 
			
		||||
	for _, v := range p.unitsMode {
 | 
			
		||||
		if v >= perm_model.AccessModeRead {
 | 
			
		||||
@@ -267,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
	perm.units = repo.Units
 | 
			
		||||
 | 
			
		||||
	// anonymous user visit private repo.
 | 
			
		||||
	// TODO: anonymous user visit public unit of private repo???
 | 
			
		||||
	if user == nil && repo.IsPrivate {
 | 
			
		||||
		perm.AccessMode = perm_model.AccessModeNone
 | 
			
		||||
		return perm, nil
 | 
			
		||||
@@ -286,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Prevent strangers from checking out public repo of private organization/users
 | 
			
		||||
	// Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself
 | 
			
		||||
	// Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself
 | 
			
		||||
	// TODO: rename it to "IsOwnerVisibleToDoer"
 | 
			
		||||
	if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator {
 | 
			
		||||
		perm.AccessMode = perm_model.AccessModeNone
 | 
			
		||||
		return perm, nil
 | 
			
		||||
@@ -304,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
		return perm, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// plain user
 | 
			
		||||
	// plain user TODO: this check should be replaced, only need to check collaborator access mode
 | 
			
		||||
	perm.AccessMode, err = accessLevel(ctx, user, repo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return perm, err
 | 
			
		||||
@@ -314,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
		return perm, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// now: the owner is visible to doer, if the repo is public, then the min access mode is read
 | 
			
		||||
	minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone)
 | 
			
		||||
	perm.AccessMode = max(perm.AccessMode, minAccessMode)
 | 
			
		||||
 | 
			
		||||
	// get units mode from teams
 | 
			
		||||
	teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return perm, err
 | 
			
		||||
	}
 | 
			
		||||
	if len(teams) == 0 {
 | 
			
		||||
		return perm, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
 | 
			
		||||
 | 
			
		||||
	// Collaborators on organization
 | 
			
		||||
@@ -323,12 +337,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// get units mode from teams
 | 
			
		||||
	teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return perm, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if user in an owner team
 | 
			
		||||
	for _, team := range teams {
 | 
			
		||||
		if team.HasAdminAccess() {
 | 
			
		||||
@@ -339,19 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range repo.Units {
 | 
			
		||||
		var found bool
 | 
			
		||||
		for _, team := range teams {
 | 
			
		||||
			unitAccessMode := minAccessMode
 | 
			
		||||
			if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist {
 | 
			
		||||
				perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode)
 | 
			
		||||
				found = true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
 | 
			
		||||
		if !found && !repo.IsPrivate && !user.IsRestricted {
 | 
			
		||||
			if _, ok := perm.unitsMode[u.Type]; !ok {
 | 
			
		||||
				perm.unitsMode[u.Type] = perm_model.AccessModeRead
 | 
			
		||||
				unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode)
 | 
			
		||||
			}
 | 
			
		||||
			perm.unitsMode[u.Type] = unitAccessMode
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,16 @@ package access
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	"code.gitea.io/gitea/models/organization"
 | 
			
		||||
	perm_model "code.gitea.io/gitea/models/perm"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestHasAnyUnitAccess(t *testing.T) {
 | 
			
		||||
@@ -152,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
	assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetUserRepoPermission(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
	repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
 | 
			
		||||
	require.NoError(t, repo32.LoadOwner(ctx))
 | 
			
		||||
	require.True(t, repo32.Owner.IsOrganization())
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}))
 | 
			
		||||
	org := repo32.Owner
 | 
			
		||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 | 
			
		||||
	team := &organization.Team{OrgID: org.ID, LowerName: "test_team"}
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, team))
 | 
			
		||||
 | 
			
		||||
	t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
 | 
			
		||||
		require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
 | 
			
		||||
		perm, err := GetUserRepoPermission(ctx, repo32, user)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
 | 
			
		||||
		assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
 | 
			
		||||
	t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
 | 
			
		||||
		perm, err := GetUserRepoPermission(ctx, repo32, user)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
 | 
			
		||||
	require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
 | 
			
		||||
	t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
 | 
			
		||||
		perm, err := GetUserRepoPermission(ctx, repo32, user)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
 | 
			
		||||
		assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues])
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,14 @@ package pull
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
 | 
			
		||||
@@ -76,7 +78,10 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
 | 
			
		||||
		return false, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
 | 
			
		||||
	doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
 | 
			
		||||
	if errors.Is(err, util.ErrNotExist) {
 | 
			
		||||
		doer, err = user_model.NewGhostUser(), nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1176,12 +1176,14 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
 | 
			
		||||
 | 
			
		||||
	needCheckEmails := make(container.Set[string])
 | 
			
		||||
	needCheckUserNames := make(container.Set[string])
 | 
			
		||||
	noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress)
 | 
			
		||||
	for _, email := range emails {
 | 
			
		||||
		if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) {
 | 
			
		||||
			username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress)
 | 
			
		||||
			needCheckUserNames.Add(strings.ToLower(username))
 | 
			
		||||
		emailLower := strings.ToLower(email)
 | 
			
		||||
		if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok {
 | 
			
		||||
			needCheckUserNames.Add(noReplyUserNameLower)
 | 
			
		||||
			needCheckEmails.Add(emailLower)
 | 
			
		||||
		} else {
 | 
			
		||||
			needCheckEmails.Add(strings.ToLower(email))
 | 
			
		||||
			needCheckEmails.Add(emailLower)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,10 @@ func TestUserEmails(t *testing.T) {
 | 
			
		||||
				testGetUserByEmail(t, c.Email, c.UID)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
		t.Run("NoReplyConflict", func(t *testing.T) {
 | 
			
		||||
			setting.Service.NoReplyAddress = "example.com"
 | 
			
		||||
			testGetUserByEmail(t, "user1-2@example.COM", 1)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,22 +6,26 @@ package fileicon
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func BasicThemeIcon(entry *git.TreeEntry) template.HTML {
 | 
			
		||||
func BasicEntryIconName(entry *EntryInfo) string {
 | 
			
		||||
	svgName := "octicon-file"
 | 
			
		||||
	switch {
 | 
			
		||||
	case entry.IsLink():
 | 
			
		||||
	case entry.EntryMode.IsLink():
 | 
			
		||||
		svgName = "octicon-file-symlink-file"
 | 
			
		||||
		if te, err := entry.FollowLink(); err == nil && te.IsDir() {
 | 
			
		||||
		if entry.SymlinkToMode.IsDir() {
 | 
			
		||||
			svgName = "octicon-file-directory-symlink"
 | 
			
		||||
		}
 | 
			
		||||
	case entry.IsDir():
 | 
			
		||||
		svgName = "octicon-file-directory-fill"
 | 
			
		||||
	case entry.IsSubModule():
 | 
			
		||||
	case entry.EntryMode.IsDir():
 | 
			
		||||
		svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill")
 | 
			
		||||
	case entry.EntryMode.IsSubModule():
 | 
			
		||||
		svgName = "octicon-file-submodule"
 | 
			
		||||
	}
 | 
			
		||||
	return svg.RenderHTML(svgName)
 | 
			
		||||
	return svgName
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BasicEntryIconHTML(entry *EntryInfo) template.HTML {
 | 
			
		||||
	return svg.RenderHTML(BasicEntryIconName(entry))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								modules/fileicon/entry.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								modules/fileicon/entry.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package fileicon
 | 
			
		||||
 | 
			
		||||
import "code.gitea.io/gitea/modules/git"
 | 
			
		||||
 | 
			
		||||
type EntryInfo struct {
 | 
			
		||||
	FullName      string
 | 
			
		||||
	EntryMode     git.EntryMode
 | 
			
		||||
	SymlinkToMode git.EntryMode
 | 
			
		||||
	IsOpen        bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo {
 | 
			
		||||
	ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()}
 | 
			
		||||
	if gitEntry.IsLink() {
 | 
			
		||||
		if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() {
 | 
			
		||||
			ret.SymlinkToMode = te.Mode()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EntryInfoFolder() *EntryInfo {
 | 
			
		||||
	return &EntryInfo{EntryMode: git.EntryModeTree}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EntryInfoFolderOpen() *EntryInfo {
 | 
			
		||||
	return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true}
 | 
			
		||||
}
 | 
			
		||||
@@ -9,11 +9,12 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/json"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/options"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/svg"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type materialIconRulesData struct {
 | 
			
		||||
@@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg,
 | 
			
		||||
	}
 | 
			
		||||
	svgID := "svg-mfi-" + name
 | 
			
		||||
	svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"`
 | 
			
		||||
	svgHTML := template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
 | 
			
		||||
	if p == nil {
 | 
			
		||||
		return svgHTML
 | 
			
		||||
	}
 | 
			
		||||
	if p.IconSVGs[svgID] == "" {
 | 
			
		||||
		p.IconSVGs[svgID] = template.HTML(`<svg id="` + svgID + `" ` + svgCommonAttrs + svg[4:])
 | 
			
		||||
		p.IconSVGs[svgID] = svgHTML
 | 
			
		||||
	}
 | 
			
		||||
	return template.HTML(`<svg ` + svgCommonAttrs + `><use xlink:href="#` + svgID + `"></use></svg>`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML {
 | 
			
		||||
func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML {
 | 
			
		||||
	if m.rules == nil {
 | 
			
		||||
		return BasicThemeIcon(entry)
 | 
			
		||||
		return BasicEntryIconHTML(entry)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if entry.IsLink() {
 | 
			
		||||
		if te, err := entry.FollowLink(); err == nil && te.IsDir() {
 | 
			
		||||
	if entry.EntryMode.IsLink() {
 | 
			
		||||
		if entry.SymlinkToMode.IsDir() {
 | 
			
		||||
			// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
			return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink")
 | 
			
		||||
		}
 | 
			
		||||
		return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	name := m.findIconNameByGit(entry)
 | 
			
		||||
	// the material icon pack's "folder" icon doesn't look good, so use our built-in one
 | 
			
		||||
	// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
	if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" {
 | 
			
		||||
		// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
		extraClass := "octicon-file"
 | 
			
		||||
		switch {
 | 
			
		||||
		case entry.IsDir():
 | 
			
		||||
			extraClass = "octicon-file-directory-fill"
 | 
			
		||||
		case entry.IsSubModule():
 | 
			
		||||
			extraClass = "octicon-file-submodule"
 | 
			
		||||
	name := m.FindIconName(entry)
 | 
			
		||||
	iconSVG := m.svgs[name]
 | 
			
		||||
	if iconSVG == "" {
 | 
			
		||||
		name = "file"
 | 
			
		||||
		if entry.EntryMode.IsDir() {
 | 
			
		||||
			name = util.Iif(entry.IsOpen, "folder-open", "folder")
 | 
			
		||||
		}
 | 
			
		||||
		iconSVG = m.svgs[name]
 | 
			
		||||
		if iconSVG == "" {
 | 
			
		||||
			setting.PanicInDevOrTesting("missing file icon for %s", name)
 | 
			
		||||
		}
 | 
			
		||||
		return m.renderFileIconSVG(p, name, iconSVG, extraClass)
 | 
			
		||||
	}
 | 
			
		||||
	// TODO: use an interface or wrapper for git.Entry to make the code testable.
 | 
			
		||||
	return BasicThemeIcon(entry)
 | 
			
		||||
 | 
			
		||||
	// keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work
 | 
			
		||||
	extraClass := "octicon-file"
 | 
			
		||||
	switch {
 | 
			
		||||
	case entry.EntryMode.IsDir():
 | 
			
		||||
		extraClass = BasicEntryIconName(entry)
 | 
			
		||||
	case entry.EntryMode.IsSubModule():
 | 
			
		||||
		extraClass = "octicon-file-submodule"
 | 
			
		||||
	}
 | 
			
		||||
	return m.renderFileIconSVG(p, name, iconSVG, extraClass)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
 | 
			
		||||
@@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string {
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
 | 
			
		||||
	fileNameLower := strings.ToLower(path.Base(name))
 | 
			
		||||
	if isDir {
 | 
			
		||||
func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string {
 | 
			
		||||
	if entry.EntryMode.IsSubModule() {
 | 
			
		||||
		return "folder-git"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fileNameLower := strings.ToLower(path.Base(entry.FullName))
 | 
			
		||||
	if entry.EntryMode.IsDir() {
 | 
			
		||||
		if s, ok := m.rules.FolderNames[fileNameLower]; ok {
 | 
			
		||||
			return s
 | 
			
		||||
		}
 | 
			
		||||
		return "folder"
 | 
			
		||||
		return util.Iif(entry.IsOpen, "folder-open", "folder")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s, ok := m.rules.FileNames[fileNameLower]; ok {
 | 
			
		||||
@@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string {
 | 
			
		||||
 | 
			
		||||
	return "file"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string {
 | 
			
		||||
	if entry.IsSubModule() {
 | 
			
		||||
		return "folder-git"
 | 
			
		||||
	}
 | 
			
		||||
	return m.FindIconName(entry.Name(), entry.IsDir())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	"code.gitea.io/gitea/modules/fileicon"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
@@ -19,8 +20,8 @@ func TestMain(m *testing.M) {
 | 
			
		||||
func TestFindIconName(t *testing.T) {
 | 
			
		||||
	unittest.PrepareTestEnv(t)
 | 
			
		||||
	p := fileicon.DefaultMaterialIconProvider()
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName("foo.php", false))
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName("foo.PHP", false))
 | 
			
		||||
	assert.Equal(t, "javascript", p.FindIconName("foo.js", false))
 | 
			
		||||
	assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false))
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
	assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
	assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
	assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob}))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
 | 
			
		||||
	return template.HTML(sb.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module
 | 
			
		||||
 | 
			
		||||
func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
 | 
			
		||||
func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
 | 
			
		||||
	if setting.UI.FileIconTheme == "material" {
 | 
			
		||||
		return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
 | 
			
		||||
		return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
 | 
			
		||||
	}
 | 
			
		||||
	return BasicThemeIcon(entry)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML {
 | 
			
		||||
	// TODO: add "open icon" support
 | 
			
		||||
	if setting.UI.FileIconTheme == "material" {
 | 
			
		||||
		return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry)
 | 
			
		||||
	}
 | 
			
		||||
	return BasicThemeIcon(entry)
 | 
			
		||||
	return BasicEntryIconHTML(entry)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ const (
 | 
			
		||||
	GitlabLanguage        = "gitlab-language"
 | 
			
		||||
	Lockable              = "lockable"
 | 
			
		||||
	Filter                = "filter"
 | 
			
		||||
	Diff                  = "diff"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var LinguistAttributes = []string{
 | 
			
		||||
 
 | 
			
		||||
@@ -9,3 +9,15 @@ type CommitInfo struct {
 | 
			
		||||
	Commit        *Commit
 | 
			
		||||
	SubmoduleFile *CommitSubmoduleFile
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetCommitInfoSubmoduleFile(repoLink, fullPath string, commit *Commit, refCommitID ObjectID) (*CommitSubmoduleFile, error) {
 | 
			
		||||
	submodule, err := commit.GetSubModule(fullPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if submodule == nil {
 | 
			
		||||
		// unable to find submodule from ".gitmodules" file
 | 
			
		||||
		return NewCommitSubmoduleFile(repoLink, fullPath, "", refCommitID.String()), nil
 | 
			
		||||
	}
 | 
			
		||||
	return NewCommitSubmoduleFile(repoLink, fullPath, submodule.URL, refCommitID.String()), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
	entryPaths := make([]string, len(tes)+1)
 | 
			
		||||
	// Get the commit for the treePath itself
 | 
			
		||||
	entryPaths[0] = ""
 | 
			
		||||
@@ -71,22 +71,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
 | 
			
		||||
			commitsInfo[i].Commit = entryCommit
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the entry is a submodule add a submodule file for this
 | 
			
		||||
		// If the entry is a submodule, add a submodule file for this
 | 
			
		||||
		if entry.IsSubModule() {
 | 
			
		||||
			subModuleURL := ""
 | 
			
		||||
			var fullPath string
 | 
			
		||||
			if len(treePath) > 0 {
 | 
			
		||||
				fullPath = treePath + "/" + entry.Name()
 | 
			
		||||
			} else {
 | 
			
		||||
				fullPath = entry.Name()
 | 
			
		||||
			}
 | 
			
		||||
			if subModule, err := commit.GetSubModule(fullPath); err != nil {
 | 
			
		||||
			commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			} else if subModule != nil {
 | 
			
		||||
				subModuleURL = subModule.URL
 | 
			
		||||
			}
 | 
			
		||||
			subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
 | 
			
		||||
			commitsInfo[i].SubmoduleFile = subModuleFile
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetCommitsInfo gets information of all commits that are corresponding to these entries
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
func (tes Entries) GetCommitsInfo(ctx context.Context, repoLink string, commit *Commit, treePath string) ([]CommitInfo, *Commit, error) {
 | 
			
		||||
	entryPaths := make([]string, len(tes)+1)
 | 
			
		||||
	// Get the commit for the treePath itself
 | 
			
		||||
	entryPaths[0] = ""
 | 
			
		||||
@@ -65,22 +65,12 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
 | 
			
		||||
			log.Debug("missing commit for %s", entry.Name())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// If the entry is a submodule add a submodule file for this
 | 
			
		||||
		// If the entry is a submodule, add a submodule file for this
 | 
			
		||||
		if entry.IsSubModule() {
 | 
			
		||||
			subModuleURL := ""
 | 
			
		||||
			var fullPath string
 | 
			
		||||
			if len(treePath) > 0 {
 | 
			
		||||
				fullPath = treePath + "/" + entry.Name()
 | 
			
		||||
			} else {
 | 
			
		||||
				fullPath = entry.Name()
 | 
			
		||||
			}
 | 
			
		||||
			if subModule, err := commit.GetSubModule(fullPath); err != nil {
 | 
			
		||||
			commitsInfo[i].SubmoduleFile, err = GetCommitInfoSubmoduleFile(repoLink, path.Join(treePath, entry.Name()), commit, entry.ID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			} else if subModule != nil {
 | 
			
		||||
				subModuleURL = subModule.URL
 | 
			
		||||
			}
 | 
			
		||||
			subModuleFile := NewCommitSubmoduleFile(subModuleURL, entry.ID.String())
 | 
			
		||||
			commitsInfo[i].SubmoduleFile = subModuleFile
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -82,7 +83,7 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// FIXME: Context.TODO() - if graceful has started we should use its Shutdown context otherwise use install signals in TestMain.
 | 
			
		||||
		commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), commit, testCase.Path)
 | 
			
		||||
		commitsInfo, treeCommit, err := entries.GetCommitsInfo(t.Context(), "/any/repo-link", commit, testCase.Path)
 | 
			
		||||
		assert.NoError(t, err, "Unable to get commit information for entries of subtree: %s in commit: %s from testcase due to error: %v", testCase.Path, testCase.CommitID, err)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.FailNow()
 | 
			
		||||
@@ -120,6 +121,23 @@ func TestEntries_GetCommitsInfo(t *testing.T) {
 | 
			
		||||
	defer clonedRepo1.Close()
 | 
			
		||||
 | 
			
		||||
	testGetCommitsInfo(t, clonedRepo1)
 | 
			
		||||
 | 
			
		||||
	t.Run("NonExistingSubmoduleAsNil", func(t *testing.T) {
 | 
			
		||||
		commit, err := bareRepo1.GetCommit("HEAD")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		treeEntry, err := commit.GetTreeEntryByPath("file1.txt")
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		cisf, err := GetCommitInfoSubmoduleFile("/any/repo-link", "file1.txt", commit, treeEntry.ID)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		assert.Equal(t, &CommitSubmoduleFile{
 | 
			
		||||
			repoLink: "/any/repo-link",
 | 
			
		||||
			fullPath: "file1.txt",
 | 
			
		||||
			refURL:   "",
 | 
			
		||||
			refID:    "e2129701f1a4d54dc44f03c93bca0a2aec7c5449",
 | 
			
		||||
		}, cisf)
 | 
			
		||||
		// since there is no refURL, it means that the submodule info doesn't exist, so it won't have a web link
 | 
			
		||||
		assert.Nil(t, cisf.SubmoduleWebLinkTree(t.Context()))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
 | 
			
		||||
@@ -159,7 +177,7 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) {
 | 
			
		||||
		b.ResetTimer()
 | 
			
		||||
		b.Run(benchmark.name, func(b *testing.B) {
 | 
			
		||||
			for b.Loop() {
 | 
			
		||||
				_, _, err := entries.GetCommitsInfo(b.Context(), commit, "")
 | 
			
		||||
				_, _, err := entries.GetCommitsInfo(b.Context(), "/any/repo-link", commit, "")
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					b.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,8 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
 | 
			
		||||
	return c.submoduleCache, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSubModule get the submodule according entry name
 | 
			
		||||
// GetSubModule gets the submodule by the entry name.
 | 
			
		||||
// It returns "nil, nil" if the submodule does not exist, caller should always remember to check the "nil"
 | 
			
		||||
func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
 | 
			
		||||
	modules, err := c.GetSubModules()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,49 +6,64 @@ package git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	giturl "code.gitea.io/gitea/modules/git/url"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CommitSubmoduleFile represents a file with submodule type.
 | 
			
		||||
type CommitSubmoduleFile struct {
 | 
			
		||||
	refURL    string
 | 
			
		||||
	parsedURL *giturl.RepositoryURL
 | 
			
		||||
	parsed    bool
 | 
			
		||||
	refID     string
 | 
			
		||||
	repoLink  string
 | 
			
		||||
	repoLink string
 | 
			
		||||
	fullPath string
 | 
			
		||||
	refURL   string
 | 
			
		||||
	refID    string
 | 
			
		||||
 | 
			
		||||
	parsed           bool
 | 
			
		||||
	parsedTargetLink string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewCommitSubmoduleFile create a new submodule file
 | 
			
		||||
func NewCommitSubmoduleFile(refURL, refID string) *CommitSubmoduleFile {
 | 
			
		||||
	return &CommitSubmoduleFile{refURL: refURL, refID: refID}
 | 
			
		||||
func NewCommitSubmoduleFile(repoLink, fullPath, refURL, refID string) *CommitSubmoduleFile {
 | 
			
		||||
	return &CommitSubmoduleFile{repoLink: repoLink, fullPath: fullPath, refURL: refURL, refID: refID}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RefID returns the commit ID of the submodule, it returns empty string for nil receiver
 | 
			
		||||
func (sf *CommitSubmoduleFile) RefID() string {
 | 
			
		||||
	return sf.refID // this function is only used in templates
 | 
			
		||||
	if sf == nil {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	return sf.refID
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubmoduleWebLink tries to make some web links for a submodule, it also works on "nil" receiver
 | 
			
		||||
func (sf *CommitSubmoduleFile) SubmoduleWebLink(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
 | 
			
		||||
	if sf == nil {
 | 
			
		||||
func (sf *CommitSubmoduleFile) getWebLinkInTargetRepo(ctx context.Context, moreLinkPath string) *SubmoduleWebLink {
 | 
			
		||||
	if sf == nil || sf.refURL == "" {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(sf.refURL, "../") {
 | 
			
		||||
		targetLink := path.Join(sf.repoLink, sf.refURL)
 | 
			
		||||
		return &SubmoduleWebLink{RepoWebLink: targetLink, CommitWebLink: targetLink + moreLinkPath}
 | 
			
		||||
	}
 | 
			
		||||
	if !sf.parsed {
 | 
			
		||||
		sf.parsed = true
 | 
			
		||||
		parsedURL, err := giturl.ParseRepositoryURL(ctx, sf.refURL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		sf.parsedURL = parsedURL
 | 
			
		||||
		sf.repoLink = giturl.MakeRepositoryWebLink(sf.parsedURL)
 | 
			
		||||
		sf.parsedTargetLink = giturl.MakeRepositoryWebLink(parsedURL)
 | 
			
		||||
	}
 | 
			
		||||
	var commitLink string
 | 
			
		||||
	if len(optCommitID) == 2 {
 | 
			
		||||
		commitLink = sf.repoLink + "/compare/" + optCommitID[0] + "..." + optCommitID[1]
 | 
			
		||||
	} else if len(optCommitID) == 1 {
 | 
			
		||||
		commitLink = sf.repoLink + "/tree/" + optCommitID[0]
 | 
			
		||||
	} else {
 | 
			
		||||
		commitLink = sf.repoLink + "/tree/" + sf.refID
 | 
			
		||||
	}
 | 
			
		||||
	return &SubmoduleWebLink{RepoWebLink: sf.repoLink, CommitWebLink: commitLink}
 | 
			
		||||
	return &SubmoduleWebLink{RepoWebLink: sf.parsedTargetLink, CommitWebLink: sf.parsedTargetLink + moreLinkPath}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubmoduleWebLinkTree tries to make the submodule's tree link in its own repo, it also works on "nil" receiver
 | 
			
		||||
// It returns nil if the submodule does not have a valid URL or is nil
 | 
			
		||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkTree(ctx context.Context, optCommitID ...string) *SubmoduleWebLink {
 | 
			
		||||
	return sf.getWebLinkInTargetRepo(ctx, "/tree/"+util.OptionalArg(optCommitID, sf.RefID()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubmoduleWebLinkCompare tries to make the submodule's compare link in its own repo, it also works on "nil" receiver
 | 
			
		||||
// It returns nil if the submodule does not have a valid URL or is nil
 | 
			
		||||
func (sf *CommitSubmoduleFile) SubmoduleWebLinkCompare(ctx context.Context, commitID1, commitID2 string) *SubmoduleWebLink {
 | 
			
		||||
	return sf.getWebLinkInTargetRepo(ctx, "/compare/"+commitID1+"..."+commitID2)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,20 +10,31 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCommitSubmoduleLink(t *testing.T) {
 | 
			
		||||
	sf := NewCommitSubmoduleFile("git@github.com:user/repo.git", "aaaa")
 | 
			
		||||
	assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkTree(t.Context()))
 | 
			
		||||
	assert.Nil(t, (*CommitSubmoduleFile)(nil).SubmoduleWebLinkCompare(t.Context(), "", ""))
 | 
			
		||||
	assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkTree(t.Context()))
 | 
			
		||||
	assert.Nil(t, (&CommitSubmoduleFile{}).SubmoduleWebLinkCompare(t.Context(), "", ""))
 | 
			
		||||
 | 
			
		||||
	wl := sf.SubmoduleWebLink(t.Context())
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
 | 
			
		||||
	t.Run("GitHubRepo", func(t *testing.T) {
 | 
			
		||||
		sf := NewCommitSubmoduleFile("/any/repo-link", "full-path", "git@github.com:user/repo.git", "aaaa")
 | 
			
		||||
		wl := sf.SubmoduleWebLinkTree(t.Context())
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo/tree/aaaa", wl.CommitWebLink)
 | 
			
		||||
 | 
			
		||||
	wl = sf.SubmoduleWebLink(t.Context(), "1111")
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo/tree/1111", wl.CommitWebLink)
 | 
			
		||||
		wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	wl = sf.SubmoduleWebLink(t.Context(), "1111", "2222")
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo", wl.RepoWebLink)
 | 
			
		||||
	assert.Equal(t, "https://github.com/user/repo/compare/1111...2222", wl.CommitWebLink)
 | 
			
		||||
	t.Run("RelativePath", func(t *testing.T) {
 | 
			
		||||
		sf := NewCommitSubmoduleFile("/subpath/any/repo-home-link", "full-path", "../../user/repo", "aaaa")
 | 
			
		||||
		wl := sf.SubmoduleWebLinkTree(t.Context())
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo/tree/aaaa", wl.CommitWebLink)
 | 
			
		||||
 | 
			
		||||
	wl = (*CommitSubmoduleFile)(nil).SubmoduleWebLink(t.Context())
 | 
			
		||||
	assert.Nil(t, wl)
 | 
			
		||||
		sf = NewCommitSubmoduleFile("/subpath/any/repo-home-link", "dir/submodule", "../../user/repo", "aaaa")
 | 
			
		||||
		wl = sf.SubmoduleWebLinkCompare(t.Context(), "1111", "2222")
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo", wl.RepoWebLink)
 | 
			
		||||
		assert.Equal(t, "/subpath/user/repo/compare/1111...2222", wl.CommitWebLink)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -99,9 +99,9 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseDiffHunkString parse the diffhunk content and return
 | 
			
		||||
func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) {
 | 
			
		||||
	ss := strings.Split(diffhunk, "@@")
 | 
			
		||||
// ParseDiffHunkString parse the diff hunk content and return
 | 
			
		||||
func ParseDiffHunkString(diffHunk string) (leftLine, leftHunk, rightLine, rightHunk int) {
 | 
			
		||||
	ss := strings.Split(diffHunk, "@@")
 | 
			
		||||
	ranges := strings.Split(ss[1][1:], " ")
 | 
			
		||||
	leftRange := strings.Split(ranges[0], ",")
 | 
			
		||||
	leftLine, _ = strconv.Atoi(leftRange[0][1:])
 | 
			
		||||
@@ -112,14 +112,19 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu
 | 
			
		||||
		rightRange := strings.Split(ranges[1], ",")
 | 
			
		||||
		rightLine, _ = strconv.Atoi(rightRange[0])
 | 
			
		||||
		if len(rightRange) > 1 {
 | 
			
		||||
			righHunk, _ = strconv.Atoi(rightRange[1])
 | 
			
		||||
			rightHunk, _ = strconv.Atoi(rightRange[1])
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Debug("Parse line number failed: %v", diffhunk)
 | 
			
		||||
		log.Debug("Parse line number failed: %v", diffHunk)
 | 
			
		||||
		rightLine = leftLine
 | 
			
		||||
		righHunk = leftHunk
 | 
			
		||||
		rightHunk = leftHunk
 | 
			
		||||
	}
 | 
			
		||||
	return leftLine, leftHunk, rightLine, righHunk
 | 
			
		||||
	if rightLine == 0 {
 | 
			
		||||
		// FIXME: GIT-DIFF-CUT-BUG search this tag to see details
 | 
			
		||||
		// this is only a hacky patch, the rightLine&rightHunk might still be incorrect in some cases.
 | 
			
		||||
		rightLine++
 | 
			
		||||
	}
 | 
			
		||||
	return leftLine, leftHunk, rightLine, rightHunk
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]
 | 
			
		||||
@@ -270,6 +275,12 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
 | 
			
		||||
			oldNumOfLines++
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
 | 
			
		||||
	// FIXME: GIT-DIFF-CUT-BUG But there is a bug in CutDiffAroundLine, then the "Patch" stored in the comment model becomes "@@ -1,1 +0,4 @@"
 | 
			
		||||
	// It may generate incorrect results for difference cases, for example: delete 2 line add 1 line, delete 2 line add 2 line etc, need to double check.
 | 
			
		||||
	// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
 | 
			
		||||
 | 
			
		||||
	// construct the new hunk header
 | 
			
		||||
	newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
 | 
			
		||||
		oldBegin, oldNumOfLines, newBegin, newNumOfLines)
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,31 @@ func (e EntryMode) String() string {
 | 
			
		||||
	return strconv.FormatInt(int64(e), 8)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsSubModule if the entry is a sub module
 | 
			
		||||
func (e EntryMode) IsSubModule() bool {
 | 
			
		||||
	return e == EntryModeCommit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsDir if the entry is a sub dir
 | 
			
		||||
func (e EntryMode) IsDir() bool {
 | 
			
		||||
	return e == EntryModeTree
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsLink if the entry is a symlink
 | 
			
		||||
func (e EntryMode) IsLink() bool {
 | 
			
		||||
	return e == EntryModeSymlink
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsRegular if the entry is a regular file
 | 
			
		||||
func (e EntryMode) IsRegular() bool {
 | 
			
		||||
	return e == EntryModeBlob
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsExecutable if the entry is an executable file (not necessarily binary)
 | 
			
		||||
func (e EntryMode) IsExecutable() bool {
 | 
			
		||||
	return e == EntryModeExec
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseEntryMode(mode string) (EntryMode, error) {
 | 
			
		||||
	switch mode {
 | 
			
		||||
	case "000000":
 | 
			
		||||
 
 | 
			
		||||
@@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 {
 | 
			
		||||
 | 
			
		||||
// IsSubModule if the entry is a sub module
 | 
			
		||||
func (te *TreeEntry) IsSubModule() bool {
 | 
			
		||||
	return te.entryMode == EntryModeCommit
 | 
			
		||||
	return te.entryMode.IsSubModule()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsDir if the entry is a sub dir
 | 
			
		||||
func (te *TreeEntry) IsDir() bool {
 | 
			
		||||
	return te.entryMode == EntryModeTree
 | 
			
		||||
	return te.entryMode.IsDir()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsLink if the entry is a symlink
 | 
			
		||||
func (te *TreeEntry) IsLink() bool {
 | 
			
		||||
	return te.entryMode == EntryModeSymlink
 | 
			
		||||
	return te.entryMode.IsLink()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsRegular if the entry is a regular file
 | 
			
		||||
func (te *TreeEntry) IsRegular() bool {
 | 
			
		||||
	return te.entryMode == EntryModeBlob
 | 
			
		||||
	return te.entryMode.IsRegular()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsExecutable if the entry is an executable file (not necessarily binary)
 | 
			
		||||
func (te *TreeEntry) IsExecutable() bool {
 | 
			
		||||
	return te.entryMode == EntryModeExec
 | 
			
		||||
	return te.entryMode.IsExecutable()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Blob returns the blob object the entry
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ObjectCache provides thread-safe cache operations.
 | 
			
		||||
@@ -106,3 +108,16 @@ func HashFilePathForWebUI(s string) string {
 | 
			
		||||
	_, _ = h.Write([]byte(s))
 | 
			
		||||
	return hex.EncodeToString(h.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SplitCommitTitleBody(commitMessage string, titleRuneLimit int) (title, body string) {
 | 
			
		||||
	title, body, _ = strings.Cut(commitMessage, "\n")
 | 
			
		||||
	title, title2 := util.EllipsisTruncateRunes(title, titleRuneLimit)
 | 
			
		||||
	if title2 != "" {
 | 
			
		||||
		if body == "" {
 | 
			
		||||
			body = title2
 | 
			
		||||
		} else {
 | 
			
		||||
			body = title2 + "\n" + body
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return title, body
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,3 +15,17 @@ func TestHashFilePathForWebUI(t *testing.T) {
 | 
			
		||||
		HashFilePathForWebUI("foobar"),
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestSplitCommitTitleBody(t *testing.T) {
 | 
			
		||||
	title, body := SplitCommitTitleBody("啊bcdefg", 4)
 | 
			
		||||
	assert.Equal(t, "啊…", title)
 | 
			
		||||
	assert.Equal(t, "…bcdefg", body)
 | 
			
		||||
 | 
			
		||||
	title, body = SplitCommitTitleBody("abcdefg\n1234567", 4)
 | 
			
		||||
	assert.Equal(t, "a…", title)
 | 
			
		||||
	assert.Equal(t, "…bcdefg\n1234567", body)
 | 
			
		||||
 | 
			
		||||
	title, body = SplitCommitTitleBody("abcdefg\n1234567", 100)
 | 
			
		||||
	assert.Equal(t, "abcdefg", title)
 | 
			
		||||
	assert.Equal(t, "1234567", body)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,7 @@ func newInternalRequestLFS(ctx context.Context, internalURL, method string, head
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	req := private.NewInternalRequest(ctx, internalURL, method)
 | 
			
		||||
	req.SetReadWriteTimeout(0)
 | 
			
		||||
	for k, v := range headers {
 | 
			
		||||
		req.Header(k, v)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,13 @@ func FromPtr[T any](v *T) Option[T] {
 | 
			
		||||
	return Some(*v)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FromMapLookup[K comparable, V any](m map[K]V, k K) Option[V] {
 | 
			
		||||
	if v, ok := m[k]; ok {
 | 
			
		||||
		return Some(v)
 | 
			
		||||
	}
 | 
			
		||||
	return None[V]()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FromNonDefault[T comparable](v T) Option[T] {
 | 
			
		||||
	var zero T
 | 
			
		||||
	if v == zero {
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,12 @@ func TestOption(t *testing.T) {
 | 
			
		||||
	opt3 := optional.FromNonDefault(1)
 | 
			
		||||
	assert.True(t, opt3.Has())
 | 
			
		||||
	assert.Equal(t, int(1), opt3.Value())
 | 
			
		||||
 | 
			
		||||
	opt4 := optional.FromMapLookup(map[string]int{"a": 1}, "a")
 | 
			
		||||
	assert.True(t, opt4.Has())
 | 
			
		||||
	assert.Equal(t, 1, opt4.Value())
 | 
			
		||||
	opt4 = optional.FromMapLookup(map[string]int{"a": 1}, "b")
 | 
			
		||||
	assert.False(t, opt4.Has())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_ParseBool(t *testing.T) {
 | 
			
		||||
 
 | 
			
		||||
@@ -41,11 +41,14 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, fmt.Errorf("GetObjectFormat: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	_, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, fmt.Errorf("UpdateRepository: %w", err)
 | 
			
		||||
 | 
			
		||||
	if repo.ObjectFormatName != objFmt.Name() {
 | 
			
		||||
		repo.ObjectFormatName = objFmt.Name()
 | 
			
		||||
		_, err = db.GetEngine(ctx).ID(repo.ID).NoAutoTime().Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, fmt.Errorf("UpdateRepository: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	repo.ObjectFormatName = objFmt.Name() // keep consistent with db
 | 
			
		||||
 | 
			
		||||
	allBranches := container.Set[string]{}
 | 
			
		||||
	{
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ var (
 | 
			
		||||
		ZombieTaskTimeout     time.Duration     `ini:"ZOMBIE_TASK_TIMEOUT"`
 | 
			
		||||
		EndlessTaskTimeout    time.Duration     `ini:"ENDLESS_TASK_TIMEOUT"`
 | 
			
		||||
		AbandonedJobTimeout   time.Duration     `ini:"ABANDONED_JOB_TIMEOUT"`
 | 
			
		||||
		SkipWorkflowStrings   []string          `ìni:"SKIP_WORKFLOW_STRINGS"`
 | 
			
		||||
		SkipWorkflowStrings   []string          `ini:"SKIP_WORKFLOW_STRINGS"`
 | 
			
		||||
	}{
 | 
			
		||||
		Enabled:             true,
 | 
			
		||||
		DefaultActionsURL:   defaultActionsURLGitHub,
 | 
			
		||||
 
 | 
			
		||||
@@ -41,3 +41,56 @@ EXTEND = true
 | 
			
		||||
	assert.Equal(t, "white rabbit", extended.Second)
 | 
			
		||||
	assert.True(t, extended.Extend)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Test_getCronSettings2 tests that getCronSettings can not handle two levels of embedding
 | 
			
		||||
func Test_getCronSettings2(t *testing.T) {
 | 
			
		||||
	type BaseStruct struct {
 | 
			
		||||
		Enabled    bool
 | 
			
		||||
		RunAtStart bool
 | 
			
		||||
		Schedule   string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	type Extended struct {
 | 
			
		||||
		BaseStruct
 | 
			
		||||
		Extend bool
 | 
			
		||||
	}
 | 
			
		||||
	type Extended2 struct {
 | 
			
		||||
		Extended
 | 
			
		||||
		Third string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	iniStr := `
 | 
			
		||||
[cron.test]
 | 
			
		||||
ENABLED = TRUE
 | 
			
		||||
RUN_AT_START = TRUE
 | 
			
		||||
SCHEDULE = @every 1h
 | 
			
		||||
EXTEND = true
 | 
			
		||||
THIRD = white rabbit
 | 
			
		||||
`
 | 
			
		||||
	cfg, err := NewConfigProviderFromData(iniStr)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	extended := &Extended2{
 | 
			
		||||
		Extended: Extended{
 | 
			
		||||
			BaseStruct: BaseStruct{
 | 
			
		||||
				Enabled:    false,
 | 
			
		||||
				RunAtStart: false,
 | 
			
		||||
				Schedule:   "@every 72h",
 | 
			
		||||
			},
 | 
			
		||||
			Extend: false,
 | 
			
		||||
		},
 | 
			
		||||
		Third: "black rabbit",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = getCronSettings(cfg, "test", extended)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// This confirms the first level of embedding works
 | 
			
		||||
	assert.Equal(t, "white rabbit", extended.Third)
 | 
			
		||||
	assert.True(t, extended.Extend)
 | 
			
		||||
 | 
			
		||||
	// This confirms 2 levels of embedding doesn't work
 | 
			
		||||
	assert.False(t, extended.Enabled)
 | 
			
		||||
	assert.False(t, extended.RunAtStart)
 | 
			
		||||
	assert.Equal(t, "@every 72h", extended.Schedule)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -275,7 +275,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
 | 
			
		||||
			HTTPAddr = filepath.Join(AppWorkPath, HTTPAddr)
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		log.Fatal("Invalid PROTOCOL %q", Protocol)
 | 
			
		||||
		log.Fatal("Invalid PROTOCOL %q", protocolCfg)
 | 
			
		||||
	}
 | 
			
		||||
	UseProxyProtocol = sec.Key("USE_PROXY_PROTOCOL").MustBool(false)
 | 
			
		||||
	ProxyProtocolTLSBridging = sec.Key("PROXY_PROTOCOL_TLS_BRIDGING").MustBool(false)
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ type Repository struct {
 | 
			
		||||
	Private       bool        `json:"private"`
 | 
			
		||||
	Fork          bool        `json:"fork"`
 | 
			
		||||
	Template      bool        `json:"template"`
 | 
			
		||||
	Parent        *Repository `json:"parent"`
 | 
			
		||||
	Parent        *Repository `json:"parent,omitempty"`
 | 
			
		||||
	Mirror        bool        `json:"mirror"`
 | 
			
		||||
	Size          int         `json:"size"`
 | 
			
		||||
	Language      string      `json:"language"`
 | 
			
		||||
@@ -112,7 +112,7 @@ type Repository struct {
 | 
			
		||||
	ObjectFormatName string `json:"object_format_name"`
 | 
			
		||||
	// swagger:strfmt date-time
 | 
			
		||||
	MirrorUpdated time.Time     `json:"mirror_updated,omitempty"`
 | 
			
		||||
	RepoTransfer  *RepoTransfer `json:"repo_transfer"`
 | 
			
		||||
	RepoTransfer  *RepoTransfer `json:"repo_transfer,omitempty"`
 | 
			
		||||
	Topics        []string      `json:"topics"`
 | 
			
		||||
	Licenses      []string      `json:"licenses"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ func IsLikelyEllipsisLeftPart(s string) bool {
 | 
			
		||||
	return strings.HasSuffix(s, utf8Ellipsis) || strings.HasSuffix(s, asciiEllipsis)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ellipsisGuessDisplayWidth(r rune) int {
 | 
			
		||||
func ellipsisDisplayGuessWidth(r rune) int {
 | 
			
		||||
	// To make the truncated string as long as possible,
 | 
			
		||||
	// CJK/emoji chars are considered as 2-ASCII width but not 3-4 bytes width.
 | 
			
		||||
	// Here we only make the best guess (better than counting them in bytes),
 | 
			
		||||
@@ -48,13 +48,17 @@ func ellipsisGuessDisplayWidth(r rune) int {
 | 
			
		||||
// It appends "…" or "..." at the end of truncated string.
 | 
			
		||||
// It guarantees the length of the returned runes doesn't exceed the limit.
 | 
			
		||||
func EllipsisDisplayString(str string, limit int) string {
 | 
			
		||||
	s, _, _, _ := ellipsisDisplayString(str, limit)
 | 
			
		||||
	s, _, _, _ := ellipsisDisplayString(str, limit, ellipsisDisplayGuessWidth)
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EllipsisDisplayStringX works like EllipsisDisplayString while it also returns the right part
 | 
			
		||||
func EllipsisDisplayStringX(str string, limit int) (left, right string) {
 | 
			
		||||
	left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit)
 | 
			
		||||
	return ellipsisDisplayStringX(str, limit, ellipsisDisplayGuessWidth)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ellipsisDisplayStringX(str string, limit int, widthGuess func(rune) int) (left, right string) {
 | 
			
		||||
	left, offset, truncated, encounterInvalid := ellipsisDisplayString(str, limit, widthGuess)
 | 
			
		||||
	if truncated {
 | 
			
		||||
		right = str[offset:]
 | 
			
		||||
		r, _ := utf8.DecodeRune(UnsafeStringToBytes(right))
 | 
			
		||||
@@ -68,7 +72,7 @@ func EllipsisDisplayStringX(str string, limit int) (left, right string) {
 | 
			
		||||
	return left, right
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ellipsisDisplayString(str string, limit int) (res string, offset int, truncated, encounterInvalid bool) {
 | 
			
		||||
func ellipsisDisplayString(str string, limit int, widthGuess func(rune) int) (res string, offset int, truncated, encounterInvalid bool) {
 | 
			
		||||
	if len(str) <= limit {
 | 
			
		||||
		return str, len(str), false, false
 | 
			
		||||
	}
 | 
			
		||||
@@ -81,7 +85,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
 | 
			
		||||
	for i, r := range str {
 | 
			
		||||
		encounterInvalid = encounterInvalid || r == utf8.RuneError
 | 
			
		||||
		pos = i
 | 
			
		||||
		runeWidth := ellipsisGuessDisplayWidth(r)
 | 
			
		||||
		runeWidth := widthGuess(r)
 | 
			
		||||
		if used+runeWidth+3 > limit {
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
@@ -96,7 +100,7 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
 | 
			
		||||
			if nextCnt >= 4 {
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
			nextWidth += ellipsisGuessDisplayWidth(r)
 | 
			
		||||
			nextWidth += widthGuess(r)
 | 
			
		||||
			nextCnt++
 | 
			
		||||
		}
 | 
			
		||||
		if nextCnt <= 3 && used+nextWidth <= limit {
 | 
			
		||||
@@ -114,6 +118,10 @@ func ellipsisDisplayString(str string, limit int) (res string, offset int, trunc
 | 
			
		||||
	return str[:offset] + ellipsis, offset, true, encounterInvalid
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func EllipsisTruncateRunes(str string, limit int) (left, right string) {
 | 
			
		||||
	return ellipsisDisplayStringX(str, limit, func(r rune) int { return 1 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TruncateRunes returns a truncated string with given rune limit,
 | 
			
		||||
// it returns input string if its rune length doesn't exceed the limit.
 | 
			
		||||
func TruncateRunes(str string, limit int) string {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ func TestEllipsisGuessDisplayWidth(t *testing.T) {
 | 
			
		||||
		t.Run(c.r, func(t *testing.T) {
 | 
			
		||||
			w := 0
 | 
			
		||||
			for _, r := range c.r {
 | 
			
		||||
				w += ellipsisGuessDisplayWidth(r)
 | 
			
		||||
				w += ellipsisDisplayGuessWidth(r)
 | 
			
		||||
			}
 | 
			
		||||
			assert.Equal(t, c.want, w, "hex=% x", []byte(c.r))
 | 
			
		||||
		})
 | 
			
		||||
 
 | 
			
		||||
@@ -1957,7 +1957,7 @@ pulls.cmd_instruction_checkout_title = Checkout
 | 
			
		||||
pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes.
 | 
			
		||||
pulls.cmd_instruction_merge_title = Merge
 | 
			
		||||
pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea.
 | 
			
		||||
pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable
 | 
			
		||||
pulls.cmd_instruction_merge_warning = Warning: This operation cannot merge pull request because "autodetect manual merge" is not enabled.
 | 
			
		||||
pulls.clear_merge_message = Clear merge message
 | 
			
		||||
pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …".
 | 
			
		||||
 | 
			
		||||
@@ -2153,6 +2153,7 @@ settings.collaboration.write = Write
 | 
			
		||||
settings.collaboration.read = Read
 | 
			
		||||
settings.collaboration.owner = Owner
 | 
			
		||||
settings.collaboration.undefined = Undefined
 | 
			
		||||
settings.collaboration.per_unit = Unit Permissions
 | 
			
		||||
settings.hooks = Webhooks
 | 
			
		||||
settings.githooks = Git Hooks
 | 
			
		||||
settings.basic_settings = Basic Settings
 | 
			
		||||
 
 | 
			
		||||
@@ -320,6 +320,7 @@ func InitiateUploadBlob(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks
 | 
			
		||||
func GetUploadBlob(ctx *context.Context) {
 | 
			
		||||
	image := ctx.PathParam("image")
 | 
			
		||||
	uuid := ctx.PathParam("uuid")
 | 
			
		||||
 | 
			
		||||
	upload, err := packages_model.GetBlobUploadByID(ctx, uuid)
 | 
			
		||||
@@ -334,6 +335,7 @@ func GetUploadBlob(ctx *context.Context) {
 | 
			
		||||
 | 
			
		||||
	// FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578
 | 
			
		||||
	respHeaders := &containerHeaders{
 | 
			
		||||
		Location:   fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID),
 | 
			
		||||
		UploadUUID: upload.ID,
 | 
			
		||||
		Status:     http.StatusNoContent,
 | 
			
		||||
	}
 | 
			
		||||
@@ -386,7 +388,7 @@ func UploadBlob(ctx *context.Context) {
 | 
			
		||||
		UploadUUID: uploader.ID,
 | 
			
		||||
		Status:     http.StatusAccepted,
 | 
			
		||||
	}
 | 
			
		||||
	if contentRange != "" {
 | 
			
		||||
	if uploader.Size() > 0 {
 | 
			
		||||
		respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1)
 | 
			
		||||
	}
 | 
			
		||||
	setResponseHeaders(ctx.Resp, respHeaders)
 | 
			
		||||
 
 | 
			
		||||
@@ -240,7 +240,7 @@ func EditUser(ctx *context.APIContext) {
 | 
			
		||||
		Description:             optional.FromPtr(form.Description),
 | 
			
		||||
		IsActive:                optional.FromPtr(form.Active),
 | 
			
		||||
		IsAdmin:                 user_service.UpdateOptionFieldFromPtr(form.Admin),
 | 
			
		||||
		Visibility:              optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
 | 
			
		||||
		Visibility:              optional.FromMapLookup(api.VisibilityModes, form.Visibility),
 | 
			
		||||
		AllowGitHook:            optional.FromPtr(form.AllowGitHook),
 | 
			
		||||
		AllowImportLocal:        optional.FromPtr(form.AllowImportLocal),
 | 
			
		||||
		MaxRepoCreation:         optional.FromPtr(form.MaxRepoCreation),
 | 
			
		||||
 
 | 
			
		||||
@@ -228,7 +228,7 @@ func repoAssignment() func(ctx *context.APIContext) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !ctx.Repo.Permission.HasAnyUnitAccess() {
 | 
			
		||||
		if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() {
 | 
			
		||||
			ctx.APIErrorNotFound()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
@@ -1241,7 +1241,7 @@ func Routes() *web.Router {
 | 
			
		||||
				}, reqToken())
 | 
			
		||||
				m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
 | 
			
		||||
				m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
 | 
			
		||||
				m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
 | 
			
		||||
				m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
 | 
			
		||||
				m.Combo("/forks").Get(repo.ListForks).
 | 
			
		||||
					Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
 | 
			
		||||
				m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream)
 | 
			
		||||
@@ -1445,7 +1445,7 @@ func Routes() *web.Router {
 | 
			
		||||
					m.Delete("", repo.DeleteAvatar)
 | 
			
		||||
				}, reqAdmin(), reqToken())
 | 
			
		||||
 | 
			
		||||
				m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
 | 
			
		||||
				m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive)
 | 
			
		||||
			}, repoAssignment(), checkTokenPublicOnly())
 | 
			
		||||
		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -393,7 +393,7 @@ func Edit(ctx *context.APIContext) {
 | 
			
		||||
		Description:               optional.Some(form.Description),
 | 
			
		||||
		Website:                   optional.Some(form.Website),
 | 
			
		||||
		Location:                  optional.Some(form.Location),
 | 
			
		||||
		Visibility:                optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
 | 
			
		||||
		Visibility:                optional.FromMapLookup(api.VisibilityModes, form.Visibility),
 | 
			
		||||
		RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
 | 
			
		||||
	}
 | 
			
		||||
	if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -44,5 +44,5 @@ type swaggerResponseActionWorkflow struct {
 | 
			
		||||
// swagger:response ActionWorkflowList
 | 
			
		||||
type swaggerResponseActionWorkflowList struct {
 | 
			
		||||
	// in:body
 | 
			
		||||
	Body []api.ActionWorkflow `json:"body"`
 | 
			
		||||
	Body api.ActionWorkflowResponse `json:"body"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/services/context"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/feeds"
 | 
			
		||||
@@ -15,10 +16,14 @@ import (
 | 
			
		||||
 | 
			
		||||
// ShowBranchFeed shows tags and/or releases on the repo as RSS / Atom feed
 | 
			
		||||
func ShowBranchFeed(ctx *context.Context, repo *repo.Repository, formatType string) {
 | 
			
		||||
	commits, err := ctx.Repo.Commit.CommitsByRange(0, 10, "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("ShowBranchFeed", err)
 | 
			
		||||
		return
 | 
			
		||||
	var commits []*git.Commit
 | 
			
		||||
	var err error
 | 
			
		||||
	if ctx.Repo.Commit != nil {
 | 
			
		||||
		commits, err = ctx.Repo.Commit.CommitsByRange(0, 10, "")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("ShowBranchFeed", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	title := "Latest commits for branch " + ctx.Repo.BranchName
 | 
			
		||||
 
 | 
			
		||||
@@ -283,11 +283,22 @@ func NewTeam(ctx *context.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future,
 | 
			
		||||
// admin team won't inherit the correct admin permission for the new unit.
 | 
			
		||||
// The existing teams won't inherit the correct admin permission for the new unit.
 | 
			
		||||
// The full history is like this:
 | 
			
		||||
// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission.
 | 
			
		||||
// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs.
 | 
			
		||||
//   - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner
 | 
			
		||||
//   - Sometimes, "team unit" is used not really used and "team unit" is used.
 | 
			
		||||
//   - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both.
 | 
			
		||||
//
 | 
			
		||||
// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions.
 | 
			
		||||
//   - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units.
 | 
			
		||||
//
 | 
			
		||||
// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones.
 | 
			
		||||
func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode {
 | 
			
		||||
	unitPerms := make(map[unit_model.Type]perm.AccessMode)
 | 
			
		||||
	for _, ut := range unit_model.AllRepoUnitTypes {
 | 
			
		||||
		// Default accessmode is none
 | 
			
		||||
		// Default access mode is none
 | 
			
		||||
		unitPerms[ut] = perm.AccessModeNone
 | 
			
		||||
 | 
			
		||||
		v, ok := forms[fmt.Sprintf("unit_%d", ut)]
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import (
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/charset"
 | 
			
		||||
	"code.gitea.io/gitea/modules/fileicon"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -168,10 +169,13 @@ func Graph(ctx *context.Context) {
 | 
			
		||||
	ctx.Data["Username"] = ctx.Repo.Owner.Name
 | 
			
		||||
	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
 | 
			
		||||
 | 
			
		||||
	divOnly := ctx.FormBool("div-only")
 | 
			
		||||
	queryParams := ctx.Req.URL.Query()
 | 
			
		||||
	queryParams.Del("div-only")
 | 
			
		||||
	paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5)
 | 
			
		||||
	paginator.AddParamFromRequest(ctx.Req)
 | 
			
		||||
	paginator.AddParamFromQuery(queryParams)
 | 
			
		||||
	ctx.Data["Page"] = paginator
 | 
			
		||||
	if ctx.FormBool("div-only") {
 | 
			
		||||
	if divOnly {
 | 
			
		||||
		ctx.HTML(http.StatusOK, tplGraphDiv)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
@@ -313,7 +317,7 @@ func Diff(ctx *context.Context) {
 | 
			
		||||
		maxLines, maxFiles = -1, -1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{
 | 
			
		||||
	diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, &gitdiff.DiffOptions{
 | 
			
		||||
		AfterCommitID:      commitID,
 | 
			
		||||
		SkipTo:             ctx.FormString("skip-to"),
 | 
			
		||||
		MaxLines:           maxLines,
 | 
			
		||||
@@ -369,7 +373,11 @@ func Diff(ctx *context.Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil)
 | 
			
		||||
		renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
 | 
			
		||||
		ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
 | 
			
		||||
		ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
 | 
			
		||||
		ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll)
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/charset"
 | 
			
		||||
	csv_module "code.gitea.io/gitea/modules/csv"
 | 
			
		||||
	"code.gitea.io/gitea/modules/fileicon"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
@@ -613,7 +614,7 @@ func PrepareCompareDiff(
 | 
			
		||||
 | 
			
		||||
	fileOnly := ctx.FormBool("file-only")
 | 
			
		||||
 | 
			
		||||
	diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo,
 | 
			
		||||
	diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadRepo.Link(), ci.HeadGitRepo,
 | 
			
		||||
		&gitdiff.DiffOptions{
 | 
			
		||||
			BeforeCommitID:     beforeCommitID,
 | 
			
		||||
			AfterCommitID:      headCommitID,
 | 
			
		||||
@@ -644,7 +645,11 @@ func PrepareCompareDiff(
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, nil)
 | 
			
		||||
		renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, nil)
 | 
			
		||||
		ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
 | 
			
		||||
		ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
 | 
			
		||||
		ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/emoji"
 | 
			
		||||
	"code.gitea.io/gitea/modules/fileicon"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
@@ -642,8 +643,17 @@ func ViewPullCommits(ctx *context.Context) {
 | 
			
		||||
	ctx.HTML(http.StatusOK, tplPullCommits)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func indexCommit(commits []*git.Commit, commitID string) *git.Commit {
 | 
			
		||||
	for i := range commits {
 | 
			
		||||
		if commits[i].ID.String() == commitID {
 | 
			
		||||
			return commits[i]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ViewPullFiles render pull request changed files list page
 | 
			
		||||
func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommit string, willShowSpecifiedCommitRange, willShowSpecifiedCommit bool) {
 | 
			
		||||
func viewPullFiles(ctx *context.Context, beforeCommitID, afterCommitID string) {
 | 
			
		||||
	ctx.Data["PageIsPullList"] = true
 | 
			
		||||
	ctx.Data["PageIsPullFiles"] = true
 | 
			
		||||
 | 
			
		||||
@@ -653,11 +663,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
	}
 | 
			
		||||
	pull := issue.PullRequest
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		startCommitID string
 | 
			
		||||
		endCommitID   string
 | 
			
		||||
		gitRepo       = ctx.Repo.GitRepo
 | 
			
		||||
	)
 | 
			
		||||
	gitRepo := ctx.Repo.GitRepo
 | 
			
		||||
 | 
			
		||||
	prInfo := preparePullViewPullInfo(ctx, issue)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
@@ -667,77 +673,68 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Validate the given commit sha to show (if any passed)
 | 
			
		||||
	if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
 | 
			
		||||
		foundStartCommit := len(specifiedStartCommit) == 0
 | 
			
		||||
		foundEndCommit := len(specifiedEndCommit) == 0
 | 
			
		||||
 | 
			
		||||
		if !(foundStartCommit && foundEndCommit) {
 | 
			
		||||
			for _, commit := range prInfo.Commits {
 | 
			
		||||
				if commit.ID.String() == specifiedStartCommit {
 | 
			
		||||
					foundStartCommit = true
 | 
			
		||||
				}
 | 
			
		||||
				if commit.ID.String() == specifiedEndCommit {
 | 
			
		||||
					foundEndCommit = true
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if foundStartCommit && foundEndCommit {
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !(foundStartCommit && foundEndCommit) {
 | 
			
		||||
			ctx.NotFound(nil)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetRefCommitID", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit
 | 
			
		||||
	isSingleCommit := beforeCommitID == "" && afterCommitID != ""
 | 
			
		||||
	ctx.Data["IsShowingOnlySingleCommit"] = isSingleCommit
 | 
			
		||||
	isShowAllCommits := (beforeCommitID == "" || beforeCommitID == prInfo.MergeBase) && (afterCommitID == "" || afterCommitID == headCommitID)
 | 
			
		||||
	ctx.Data["IsShowingAllCommits"] = isShowAllCommits
 | 
			
		||||
 | 
			
		||||
	if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
 | 
			
		||||
		if len(specifiedEndCommit) > 0 {
 | 
			
		||||
			endCommitID = specifiedEndCommit
 | 
			
		||||
	if afterCommitID == "" || afterCommitID == headCommitID {
 | 
			
		||||
		afterCommitID = headCommitID
 | 
			
		||||
	}
 | 
			
		||||
	afterCommit := indexCommit(prInfo.Commits, afterCommitID)
 | 
			
		||||
	if afterCommit == nil {
 | 
			
		||||
		ctx.HTTPError(http.StatusBadRequest, "after commit not found in PR commits")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var beforeCommit *git.Commit
 | 
			
		||||
	if !isSingleCommit {
 | 
			
		||||
		if beforeCommitID == "" || beforeCommitID == prInfo.MergeBase {
 | 
			
		||||
			beforeCommitID = prInfo.MergeBase
 | 
			
		||||
			// mergebase commit is not in the list of the pull request commits
 | 
			
		||||
			beforeCommit, err = gitRepo.GetCommit(beforeCommitID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				ctx.ServerError("GetCommit", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			endCommitID = headCommitID
 | 
			
		||||
			beforeCommit = indexCommit(prInfo.Commits, beforeCommitID)
 | 
			
		||||
			if beforeCommit == nil {
 | 
			
		||||
				ctx.HTTPError(http.StatusBadRequest, "before commit not found in PR commits")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if len(specifiedStartCommit) > 0 {
 | 
			
		||||
			startCommitID = specifiedStartCommit
 | 
			
		||||
		} else {
 | 
			
		||||
			startCommitID = prInfo.MergeBase
 | 
			
		||||
		}
 | 
			
		||||
		ctx.Data["IsShowingAllCommits"] = false
 | 
			
		||||
	} else {
 | 
			
		||||
		endCommitID = headCommitID
 | 
			
		||||
		startCommitID = prInfo.MergeBase
 | 
			
		||||
		ctx.Data["IsShowingAllCommits"] = true
 | 
			
		||||
		beforeCommit, err = afterCommit.Parent(0)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("Parent", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		beforeCommitID = beforeCommit.ID.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["Username"] = ctx.Repo.Owner.Name
 | 
			
		||||
	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
 | 
			
		||||
	ctx.Data["AfterCommitID"] = endCommitID
 | 
			
		||||
	ctx.Data["BeforeCommitID"] = startCommitID
 | 
			
		||||
 | 
			
		||||
	fileOnly := ctx.FormBool("file-only")
 | 
			
		||||
	ctx.Data["MergeBase"] = prInfo.MergeBase
 | 
			
		||||
	ctx.Data["AfterCommitID"] = afterCommitID
 | 
			
		||||
	ctx.Data["BeforeCommitID"] = beforeCommitID
 | 
			
		||||
 | 
			
		||||
	maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
 | 
			
		||||
	files := ctx.FormStrings("files")
 | 
			
		||||
	fileOnly := ctx.FormBool("file-only")
 | 
			
		||||
	if fileOnly && (len(files) == 2 || len(files) == 1) {
 | 
			
		||||
		maxLines, maxFiles = -1, -1
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	diffOptions := &gitdiff.DiffOptions{
 | 
			
		||||
		AfterCommitID:      endCommitID,
 | 
			
		||||
		BeforeCommitID:     beforeCommitID,
 | 
			
		||||
		AfterCommitID:      afterCommitID,
 | 
			
		||||
		SkipTo:             ctx.FormString("skip-to"),
 | 
			
		||||
		MaxLines:           maxLines,
 | 
			
		||||
		MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
 | 
			
		||||
@@ -745,11 +742,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
		WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !willShowSpecifiedCommit {
 | 
			
		||||
		diffOptions.BeforeCommitID = startCommitID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...)
 | 
			
		||||
	diff, err := gitdiff.GetDiffForRender(ctx, ctx.Repo.RepoLink, gitRepo, diffOptions, files...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetDiff", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -760,7 +753,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
	// as the viewed information is designed to be loaded only on latest PR
 | 
			
		||||
	// diff and if you're signed in.
 | 
			
		||||
	var reviewState *pull_model.ReviewState
 | 
			
		||||
	if ctx.IsSigned && !willShowSpecifiedCommit && !willShowSpecifiedCommitRange {
 | 
			
		||||
	if ctx.IsSigned && isShowAllCommits {
 | 
			
		||||
		reviewState, err = gitdiff.SyncUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diff, diffOptions)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("SyncUserSpecificDiff", err)
 | 
			
		||||
@@ -768,7 +761,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, startCommitID, endCommitID)
 | 
			
		||||
	diffShortStat, err := gitdiff.GetDiffShortStat(ctx.Repo.GitRepo, beforeCommitID, afterCommitID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetDiffShortStat", err)
 | 
			
		||||
		return
 | 
			
		||||
@@ -815,7 +808,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
 | 
			
		||||
	if !fileOnly {
 | 
			
		||||
		// note: use mergeBase is set to false because we already have the merge base from the pull request info
 | 
			
		||||
		diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, startCommitID, endCommitID)
 | 
			
		||||
		diffTree, err := gitdiff.GetDiffTree(ctx, gitRepo, false, beforeCommitID, afterCommitID)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("GetDiffTree", err)
 | 
			
		||||
			return
 | 
			
		||||
@@ -824,23 +817,17 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
		if reviewState != nil {
 | 
			
		||||
			filesViewedState = reviewState.UpdatedFiles
 | 
			
		||||
		}
 | 
			
		||||
		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(diffTree, filesViewedState)
 | 
			
		||||
 | 
			
		||||
		renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
		ctx.PageData["DiffFileTree"] = transformDiffTreeForWeb(renderedIconPool, diffTree, filesViewedState)
 | 
			
		||||
		ctx.PageData["FolderIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
 | 
			
		||||
		ctx.PageData["FolderOpenIcon"] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolderOpen())
 | 
			
		||||
		ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["Diff"] = diff
 | 
			
		||||
	ctx.Data["DiffNotAvailable"] = diffShortStat.NumFiles == 0
 | 
			
		||||
 | 
			
		||||
	baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetCommit", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	commit, err := gitRepo.GetCommit(endCommitID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetCommit", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.IsSigned && ctx.Doer != nil {
 | 
			
		||||
		if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
 | 
			
		||||
			ctx.ServerError("CanMarkConversation", err)
 | 
			
		||||
@@ -848,7 +835,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
 | 
			
		||||
	setCompareContext(ctx, beforeCommit, afterCommit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
 | 
			
		||||
 | 
			
		||||
	assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -895,7 +882,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
	ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
 | 
			
		||||
		return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
 | 
			
		||||
	}
 | 
			
		||||
	if !willShowSpecifiedCommit && !willShowSpecifiedCommitRange && pull.Flow == issues_model.PullRequestFlowGithub {
 | 
			
		||||
	if isShowAllCommits && pull.Flow == issues_model.PullRequestFlowGithub {
 | 
			
		||||
		if err := pull.LoadHeadRepo(ctx); err != nil {
 | 
			
		||||
			ctx.ServerError("LoadHeadRepo", err)
 | 
			
		||||
			return
 | 
			
		||||
@@ -924,19 +911,17 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ViewPullFilesForSingleCommit(ctx *context.Context) {
 | 
			
		||||
	viewPullFiles(ctx, "", ctx.PathParam("sha"), true, true)
 | 
			
		||||
	// it doesn't support showing files from mergebase to the special commit
 | 
			
		||||
	// otherwise it will be ambiguous
 | 
			
		||||
	viewPullFiles(ctx, "", ctx.PathParam("sha"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ViewPullFilesForRange(ctx *context.Context) {
 | 
			
		||||
	viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"), true, false)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ViewPullFilesStartingFromCommit(ctx *context.Context) {
 | 
			
		||||
	viewPullFiles(ctx, "", ctx.PathParam("sha"), true, false)
 | 
			
		||||
	viewPullFiles(ctx, ctx.PathParam("shaFrom"), ctx.PathParam("shaTo"))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) {
 | 
			
		||||
	viewPullFiles(ctx, "", "", false, false)
 | 
			
		||||
	viewPullFiles(ctx, "", "")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatePullRequest merge PR's baseBranch into headBranch
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
@@ -89,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) {
 | 
			
		||||
	c.Data["recent_status_checks"] = contexts
 | 
			
		||||
 | 
			
		||||
	if c.Repo.Owner.IsOrganization() {
 | 
			
		||||
		teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead)
 | 
			
		||||
		teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
 | 
			
		||||
			return
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/models/organization"
 | 
			
		||||
	"code.gitea.io/gitea/models/perm"
 | 
			
		||||
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	"code.gitea.io/gitea/modules/base"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/templates"
 | 
			
		||||
@@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error {
 | 
			
		||||
	ctx.Data["Users"] = users
 | 
			
		||||
 | 
			
		||||
	if ctx.Repo.Owner.IsOrganization() {
 | 
			
		||||
		teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead)
 | 
			
		||||
		teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err)
 | 
			
		||||
			return err
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +68,7 @@ type WebDiffFileItem struct {
 | 
			
		||||
	EntryMode   string
 | 
			
		||||
	IsViewed    bool
 | 
			
		||||
	Children    []*WebDiffFileItem
 | 
			
		||||
	// TODO: add icon support in the future
 | 
			
		||||
	FileIcon    template.HTML
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebDiffFileTree is used by frontend, check the field names in frontend before changing
 | 
			
		||||
@@ -77,7 +78,7 @@ type WebDiffFileTree struct {
 | 
			
		||||
 | 
			
		||||
// transformDiffTreeForWeb transforms a gitdiff.DiffTree into a WebDiffFileTree for Web UI rendering
 | 
			
		||||
// it also takes a map of file names to their viewed state, which is used to mark files as viewed
 | 
			
		||||
func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
 | 
			
		||||
func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTree *gitdiff.DiffTree, filesViewedState map[string]pull_model.ViewedState) (dft WebDiffFileTree) {
 | 
			
		||||
	dirNodes := map[string]*WebDiffFileItem{"": &dft.TreeRoot}
 | 
			
		||||
	addItem := func(item *WebDiffFileItem) {
 | 
			
		||||
		var parentPath string
 | 
			
		||||
@@ -110,6 +111,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
 | 
			
		||||
		item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status}
 | 
			
		||||
		item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed
 | 
			
		||||
		item.NameHash = git.HashFilePathForWebUI(item.FullName)
 | 
			
		||||
		item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode})
 | 
			
		||||
 | 
			
		||||
		switch file.HeadMode {
 | 
			
		||||
		case git.EntryModeTree:
 | 
			
		||||
@@ -141,7 +143,7 @@ func transformDiffTreeForWeb(diffTree *gitdiff.DiffTree, filesViewedState map[st
 | 
			
		||||
 | 
			
		||||
func TreeViewNodes(ctx *context.Context) {
 | 
			
		||||
	renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
	results, err := files_service.GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
 | 
			
		||||
	results, err := files_service.GetTreeViewNodes(ctx, ctx.Repo.RepoLink, renderedIconPool, ctx.Repo.Commit, ctx.Repo.TreePath, ctx.FormString("sub_path"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetTreeViewNodes", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,11 @@
 | 
			
		||||
package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	pull_model "code.gitea.io/gitea/models/pull"
 | 
			
		||||
	"code.gitea.io/gitea/modules/fileicon"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/services/gitdiff"
 | 
			
		||||
 | 
			
		||||
@@ -14,7 +16,8 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestTransformDiffTreeForWeb(t *testing.T) {
 | 
			
		||||
	ret := transformDiffTreeForWeb(&gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
 | 
			
		||||
	renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
	ret := transformDiffTreeForWeb(renderedIconPool, &gitdiff.DiffTree{Files: []*gitdiff.DiffTreeRecord{
 | 
			
		||||
		{
 | 
			
		||||
			Status:   "changed",
 | 
			
		||||
			HeadPath: "dir-a/dir-a-x/file-deep",
 | 
			
		||||
@@ -29,6 +32,9 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
 | 
			
		||||
		"dir-a/dir-a-x/file-deep": pull_model.Viewed,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	mockIconForFile := func(id string) template.HTML {
 | 
			
		||||
		return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
 | 
			
		||||
	}
 | 
			
		||||
	assert.Equal(t, WebDiffFileTree{
 | 
			
		||||
		TreeRoot: WebDiffFileItem{
 | 
			
		||||
			Children: []*WebDiffFileItem{
 | 
			
		||||
@@ -44,6 +50,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
 | 
			
		||||
							NameHash:    "4acf7eef1c943a09e9f754e93ff190db8583236b",
 | 
			
		||||
							DiffStatus:  "changed",
 | 
			
		||||
							IsViewed:    true,
 | 
			
		||||
							FileIcon:    mockIconForFile(`svg-mfi-file`),
 | 
			
		||||
						},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
@@ -53,6 +60,7 @@ func TestTransformDiffTreeForWeb(t *testing.T) {
 | 
			
		||||
					FullName:    "file1",
 | 
			
		||||
					NameHash:    "60b27f004e454aca81b0480209cce5081ec52390",
 | 
			
		||||
					DiffStatus:  "added",
 | 
			
		||||
					FileIcon:    mockIconForFile(`svg-mfi-file`),
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
 
 | 
			
		||||
@@ -257,8 +257,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) {
 | 
			
		||||
	renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
	fileIcons := map[string]template.HTML{}
 | 
			
		||||
	for _, f := range files {
 | 
			
		||||
		fileIcons[f.Entry.Name()] = fileicon.RenderEntryIcon(renderedIconPool, f.Entry)
 | 
			
		||||
		fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry))
 | 
			
		||||
	}
 | 
			
		||||
	fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder())
 | 
			
		||||
	ctx.Data["FileIcons"] = fileIcons
 | 
			
		||||
	ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML()
 | 
			
		||||
}
 | 
			
		||||
@@ -298,7 +299,7 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
 | 
			
		||||
		defer cancel()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.Commit, ctx.Repo.TreePath)
 | 
			
		||||
	files, latestCommit, err := allEntries.GetCommitsInfo(commitInfoCtx, ctx.Repo.RepoLink, ctx.Repo.Commit, ctx.Repo.TreePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetCommitsInfo", err)
 | 
			
		||||
		return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -20,8 +20,8 @@ import (
 | 
			
		||||
	unit_model "code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	giturl "code.gitea.io/gitea/modules/git/url"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/htmlutil"
 | 
			
		||||
	"code.gitea.io/gitea/modules/httplib"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	repo_module "code.gitea.io/gitea/modules/repository"
 | 
			
		||||
@@ -309,34 +309,41 @@ func handleRepoEmptyOrBroken(ctx *context.Context) {
 | 
			
		||||
	ctx.Redirect(link)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleRepoViewSubmodule(ctx *context.Context, submodule *git.SubModule) {
 | 
			
		||||
	submoduleRepoURL, err := giturl.ParseRepositoryURL(ctx, submodule.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		HandleGitError(ctx, "prepareToRenderDirOrFile: ParseRepositoryURL", err)
 | 
			
		||||
func isViewHomeOnlyContent(ctx *context.Context) bool {
 | 
			
		||||
	return ctx.FormBool("only_content")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func handleRepoViewSubmodule(ctx *context.Context, commitSubmoduleFile *git.CommitSubmoduleFile) {
 | 
			
		||||
	submoduleWebLink := commitSubmoduleFile.SubmoduleWebLinkTree(ctx)
 | 
			
		||||
	if submoduleWebLink == nil {
 | 
			
		||||
		ctx.Data["NotFoundPrompt"] = ctx.Repo.TreePath
 | 
			
		||||
		ctx.NotFound(nil)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	submoduleURL := giturl.MakeRepositoryWebLink(submoduleRepoURL)
 | 
			
		||||
	if httplib.IsCurrentGiteaSiteURL(ctx, submoduleURL) {
 | 
			
		||||
		ctx.RedirectToCurrentSite(submoduleURL)
 | 
			
		||||
	} else {
 | 
			
		||||
 | 
			
		||||
	redirectLink := submoduleWebLink.CommitWebLink
 | 
			
		||||
	if isViewHomeOnlyContent(ctx) {
 | 
			
		||||
		ctx.Resp.Header().Set("Content-Type", "text/html; charset=utf-8")
 | 
			
		||||
		_, _ = ctx.Resp.Write([]byte(htmlutil.HTMLFormat(`<a href="%s">%s</a>`, redirectLink, redirectLink)))
 | 
			
		||||
	} else if !httplib.IsCurrentGiteaSiteURL(ctx, redirectLink) {
 | 
			
		||||
		// don't auto-redirect to external URL, to avoid open redirect or phishing
 | 
			
		||||
		ctx.Data["NotFoundPrompt"] = submoduleURL
 | 
			
		||||
		ctx.Data["NotFoundPrompt"] = redirectLink
 | 
			
		||||
		ctx.NotFound(nil)
 | 
			
		||||
	} else {
 | 
			
		||||
		ctx.Redirect(submoduleWebLink.CommitWebLink)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) {
 | 
			
		||||
	return func(ctx *context.Context) {
 | 
			
		||||
		if entry.IsSubModule() {
 | 
			
		||||
			submodule, err := ctx.Repo.Commit.GetSubModule(entry.Name())
 | 
			
		||||
			commitSubmoduleFile, err := git.GetCommitInfoSubmoduleFile(ctx.Repo.RepoLink, ctx.Repo.TreePath, ctx.Repo.Commit, entry.ID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				HandleGitError(ctx, "prepareToRenderDirOrFile: GetSubModule", err)
 | 
			
		||||
				HandleGitError(ctx, "prepareToRenderDirOrFile: GetCommitInfoSubmoduleFile", err)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			handleRepoViewSubmodule(ctx, submodule)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if entry.IsDir() {
 | 
			
		||||
			handleRepoViewSubmodule(ctx, commitSubmoduleFile)
 | 
			
		||||
		} else if entry.IsDir() {
 | 
			
		||||
			prepareToRenderDirectory(ctx)
 | 
			
		||||
		} else {
 | 
			
		||||
			prepareToRenderFile(ctx, entry)
 | 
			
		||||
@@ -472,7 +479,7 @@ func Home(ctx *context.Context) {
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ctx.FormBool("only_content") {
 | 
			
		||||
	if isViewHomeOnlyContent(ctx) {
 | 
			
		||||
		ctx.HTML(http.StatusOK, tplRepoViewContent)
 | 
			
		||||
	} else if len(treeNames) != 0 {
 | 
			
		||||
		ctx.HTML(http.StatusOK, tplRepoView)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	git_module "code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/services/contexttest"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
@@ -19,14 +18,20 @@ func TestViewHomeSubmoduleRedirect(t *testing.T) {
 | 
			
		||||
	unittest.PrepareTestEnv(t)
 | 
			
		||||
 | 
			
		||||
	ctx, _ := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
 | 
			
		||||
	submodule := &git_module.SubModule{Path: "test-submodule", URL: setting.AppURL + "user2/repo-other.git"}
 | 
			
		||||
	submodule := git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
 | 
			
		||||
	handleRepoViewSubmodule(ctx, submodule)
 | 
			
		||||
	assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
 | 
			
		||||
	assert.Equal(t, "/user2/repo-other", ctx.Resp.Header().Get("Location"))
 | 
			
		||||
	assert.Equal(t, "/user2/repo-other/tree/any-ref-id", ctx.Resp.Header().Get("Location"))
 | 
			
		||||
 | 
			
		||||
	ctx, _ = contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule")
 | 
			
		||||
	submodule = &git_module.SubModule{Path: "test-submodule", URL: "https://other/user2/repo-other.git"}
 | 
			
		||||
	submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "https://other/user2/repo-other.git", "any-ref-id")
 | 
			
		||||
	handleRepoViewSubmodule(ctx, submodule)
 | 
			
		||||
	// do not auto-redirect for external URLs, to avoid open redirect or phishing
 | 
			
		||||
	assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
 | 
			
		||||
 | 
			
		||||
	ctx, respWriter := contexttest.MockContext(t, "/user2/repo1/src/branch/master/test-submodule?only_content=true")
 | 
			
		||||
	submodule = git_module.NewCommitSubmoduleFile("/user2/repo1", "test-submodule", "../repo-other", "any-ref-id")
 | 
			
		||||
	handleRepoViewSubmodule(ctx, submodule)
 | 
			
		||||
	assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus())
 | 
			
		||||
	assert.Equal(t, `<a href="/user2/repo-other/tree/any-ref-id">/user2/repo-other/tree/any-ref-id</a>`, respWriter.Body.String())
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ package repo
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	gocontext "context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
@@ -666,7 +665,7 @@ func WikiPages(ctx *context.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	allEntries.CustomSort(base.NaturalSortLess)
 | 
			
		||||
 | 
			
		||||
	entries, _, err := allEntries.GetCommitsInfo(gocontext.Context(ctx), commit, treePath)
 | 
			
		||||
	entries, _, err := allEntries.GetCommitsInfo(ctx, ctx.Repo.RepoLink, commit, treePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetCommitsInfo", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -1509,7 +1509,7 @@ func registerWebRoutes(m *web.Router) {
 | 
			
		||||
			m.Group("/commits", func() {
 | 
			
		||||
				m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewPullCommits)
 | 
			
		||||
				m.Get("/list", repo.GetPullCommits)
 | 
			
		||||
				m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
 | 
			
		||||
				m.Get("/{sha:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForSingleCommit)
 | 
			
		||||
			})
 | 
			
		||||
			m.Post("/merge", context.RepoMustNotBeArchived(), web.Bind(forms.MergePullRequestForm{}), repo.MergePullRequest)
 | 
			
		||||
			m.Post("/cancel_auto_merge", context.RepoMustNotBeArchived(), repo.CancelAutoMergePullRequest)
 | 
			
		||||
@@ -1518,8 +1518,7 @@ func registerWebRoutes(m *web.Router) {
 | 
			
		||||
			m.Post("/cleanup", context.RepoMustNotBeArchived(), repo.CleanUpPullRequest)
 | 
			
		||||
			m.Group("/files", func() {
 | 
			
		||||
				m.Get("", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForAllCommitsOfPr)
 | 
			
		||||
				m.Get("/{sha:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesStartingFromCommit)
 | 
			
		||||
				m.Get("/{shaFrom:[a-f0-9]{7,40}}..{shaTo:[a-f0-9]{7,40}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
 | 
			
		||||
				m.Get("/{shaFrom:[a-f0-9]{7,64}}..{shaTo:[a-f0-9]{7,64}}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.SetShowOutdatedComments, repo.ViewPullFilesForRange)
 | 
			
		||||
				m.Group("/reviews", func() {
 | 
			
		||||
					m.Get("/new_comment", repo.RenderNewCodeCommentForm)
 | 
			
		||||
					m.Post("/comments", web.Bind(forms.CodeCommentForm{}), repo.SetShowOutdatedComments, repo.CreateCodeComment)
 | 
			
		||||
 
 | 
			
		||||
@@ -260,11 +260,6 @@ func (n *actionsNotifier) CreateIssueComment(ctx context.Context, doer *user_mod
 | 
			
		||||
func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
 | 
			
		||||
	ctx = withMethod(ctx, "UpdateComment")
 | 
			
		||||
 | 
			
		||||
	if err := c.LoadIssue(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadIssue: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.Issue.IsPull {
 | 
			
		||||
		notifyIssueCommentChange(ctx, doer, c, oldContent, webhook_module.HookEventPullRequestComment, api.HookIssueCommentEdited)
 | 
			
		||||
		return
 | 
			
		||||
@@ -275,11 +270,6 @@ func (n *actionsNotifier) UpdateComment(ctx context.Context, doer *user_model.Us
 | 
			
		||||
func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) {
 | 
			
		||||
	ctx = withMethod(ctx, "DeleteComment")
 | 
			
		||||
 | 
			
		||||
	if err := comment.LoadIssue(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadIssue: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if comment.Issue.IsPull {
 | 
			
		||||
		notifyIssueCommentChange(ctx, doer, comment, "", webhook_module.HookEventPullRequestComment, api.HookIssueCommentDeleted)
 | 
			
		||||
		return
 | 
			
		||||
@@ -288,6 +278,7 @@ func (n *actionsNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func notifyIssueCommentChange(ctx context.Context, doer *user_model.User, comment *issues_model.Comment, oldContent string, event webhook_module.HookEventType, action api.HookIssueCommentAction) {
 | 
			
		||||
	comment.Issue = nil // force issue to be loaded
 | 
			
		||||
	if err := comment.LoadIssue(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadIssue: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,12 @@ package agit
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
@@ -17,17 +19,30 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/private"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
			
		||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func parseAgitPushOptionValue(s string) string {
 | 
			
		||||
	if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok {
 | 
			
		||||
		decoded, err := base64.StdEncoding.DecodeString(base64Value)
 | 
			
		||||
		return util.Iif(err == nil, string(decoded), s)
 | 
			
		||||
	}
 | 
			
		||||
	return s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProcReceive handle proc receive work
 | 
			
		||||
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
 | 
			
		||||
	results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
 | 
			
		||||
	forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
 | 
			
		||||
	topicBranch := opts.GitPushOptions["topic"]
 | 
			
		||||
	title := strings.TrimSpace(opts.GitPushOptions["title"])
 | 
			
		||||
	description := strings.TrimSpace(opts.GitPushOptions["description"])
 | 
			
		||||
 | 
			
		||||
	// some options are base64-encoded with "{base64}" prefix if they contain new lines
 | 
			
		||||
	// other agit push options like "issue", "reviewer" and "cc" are not supported
 | 
			
		||||
	title := parseAgitPushOptionValue(opts.GitPushOptions["title"])
 | 
			
		||||
	description := parseAgitPushOptionValue(opts.GitPushOptions["description"])
 | 
			
		||||
 | 
			
		||||
	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
 | 
			
		||||
	userName := strings.ToLower(opts.UserName)
 | 
			
		||||
 | 
			
		||||
@@ -199,11 +214,37 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Store old commit ID for review staleness checking
 | 
			
		||||
		oldHeadCommitID := pr.HeadCommitID
 | 
			
		||||
 | 
			
		||||
		pr.HeadCommitID = opts.NewCommitIDs[i]
 | 
			
		||||
		if err = pull_service.UpdateRef(ctx, pr); err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Mark existing reviews as stale when PR content changes (same as regular GitHub flow)
 | 
			
		||||
		if oldHeadCommitID != opts.NewCommitIDs[i] {
 | 
			
		||||
			if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
 | 
			
		||||
				log.Error("MarkReviewsAsStale: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Dismiss all approval reviews if protected branch rule item enabled
 | 
			
		||||
			pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("GetFirstMatchProtectedBranchRule: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
			if pb != nil && pb.DismissStaleApprovals {
 | 
			
		||||
				if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
 | 
			
		||||
					log.Error("DismissApprovalReviews: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Mark reviews for the new commit as not stale
 | 
			
		||||
			if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
 | 
			
		||||
				log.Error("MarkReviewsAsNotStale: %v", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		pull_service.StartPullRequestCheckImmediately(ctx, pr)
 | 
			
		||||
		err = pr.LoadIssue(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								services/agit/agit_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								services/agit/agit_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package agit
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestParseAgitPushOptionValue(t *testing.T) {
 | 
			
		||||
	assert.Equal(t, "a", parseAgitPushOptionValue("a"))
 | 
			
		||||
	assert.Equal(t, "a", parseAgitPushOptionValue("{base64}YQ=="))
 | 
			
		||||
	assert.Equal(t, "{base64}invalid value", parseAgitPushOptionValue("{base64}invalid value"))
 | 
			
		||||
}
 | 
			
		||||
@@ -22,23 +22,21 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
	"code.gitea.io/gitea/modules/queue"
 | 
			
		||||
	"code.gitea.io/gitea/services/automergequeue"
 | 
			
		||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
			
		||||
	pull_service "code.gitea.io/gitea/services/pull"
 | 
			
		||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// prAutoMergeQueue represents a queue to handle update pull request tests
 | 
			
		||||
var prAutoMergeQueue *queue.WorkerPoolQueue[string]
 | 
			
		||||
 | 
			
		||||
// Init runs the task queue to that handles auto merges
 | 
			
		||||
func Init() error {
 | 
			
		||||
	notify_service.RegisterNotifier(NewNotifier())
 | 
			
		||||
 | 
			
		||||
	prAutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
 | 
			
		||||
	if prAutoMergeQueue == nil {
 | 
			
		||||
	automergequeue.AutoMergeQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_auto_merge", handler)
 | 
			
		||||
	if automergequeue.AutoMergeQueue == nil {
 | 
			
		||||
		return errors.New("unable to create pr_auto_merge queue")
 | 
			
		||||
	}
 | 
			
		||||
	go graceful.GetManager().RunWithCancel(prAutoMergeQueue)
 | 
			
		||||
	go graceful.GetManager().RunWithCancel(automergequeue.AutoMergeQueue)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -56,24 +54,23 @@ func handler(items ...string) []string {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addToQueue(pr *issues_model.PullRequest, sha string) {
 | 
			
		||||
	log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
 | 
			
		||||
	if err := prAutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
 | 
			
		||||
		log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ScheduleAutoMerge if schedule is false and no error, pull can be merged directly
 | 
			
		||||
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pull *issues_model.PullRequest, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) (scheduled bool, err error) {
 | 
			
		||||
	err = db.WithTx(ctx, func(ctx context.Context) error {
 | 
			
		||||
		if err := pull_model.ScheduleAutoMerge(ctx, doer, pull.ID, style, message, deleteBranchAfterMerge); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		scheduled = true
 | 
			
		||||
 | 
			
		||||
		_, err = issues_model.CreateAutoMergeComment(ctx, issues_model.CommentTypePRScheduledToAutoMerge, pull, doer)
 | 
			
		||||
		return err
 | 
			
		||||
	})
 | 
			
		||||
	// Old code made "scheduled" to be true after "ScheduleAutoMerge", but it's not right:
 | 
			
		||||
	// If the transaction rolls back, then the pull request is not scheduled to auto merge.
 | 
			
		||||
	// So we should only set "scheduled" to true if there is no error.
 | 
			
		||||
	scheduled = err == nil
 | 
			
		||||
	if scheduled {
 | 
			
		||||
		log.Trace("Pull request [%d] scheduled for auto merge with style [%s] and message [%s]", pull.ID, style, message)
 | 
			
		||||
		automergequeue.StartPRCheckAndAutoMerge(ctx, pull)
 | 
			
		||||
	}
 | 
			
		||||
	return scheduled, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -99,38 +96,12 @@ func StartPRCheckAndAutoMergeBySHA(ctx context.Context, sha string, repo *repo_m
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, pr := range pulls {
 | 
			
		||||
		addToQueue(pr, sha)
 | 
			
		||||
		automergequeue.AddToQueue(pr, sha)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
 | 
			
		||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
 | 
			
		||||
	if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pull.LoadBaseRepo(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadBaseRepo: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("OpenRepository: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer gitRepo.Close()
 | 
			
		||||
	commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetRefCommitID: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	addToQueue(pull, commitID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getPullRequestsByHeadSHA(ctx context.Context, sha string, repo *repo_model.Repository, filter func(*issues_model.PullRequest) bool) (map[int64]*issues_model.PullRequest, error) {
 | 
			
		||||
	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import (
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/repository"
 | 
			
		||||
	"code.gitea.io/gitea/services/automergequeue"
 | 
			
		||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +46,7 @@ func (n *automergeNotifier) PullReviewDismiss(ctx context.Context, doer *user_mo
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// as reviews could have blocked a pending automerge let's recheck
 | 
			
		||||
	StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
 | 
			
		||||
	automergequeue.StartPRCheckAndAutoMerge(ctx, review.Issue.PullRequest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *automergeNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								services/automergequeue/automergequeue.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								services/automergequeue/automergequeue.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package automergequeue
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	"code.gitea.io/gitea/modules/gitrepo"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/queue"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var AutoMergeQueue *queue.WorkerPoolQueue[string]
 | 
			
		||||
 | 
			
		||||
var AddToQueue = func(pr *issues_model.PullRequest, sha string) {
 | 
			
		||||
	log.Trace("Adding pullID: %d to the pull requests patch checking queue with sha %s", pr.ID, sha)
 | 
			
		||||
	if err := AutoMergeQueue.Push(fmt.Sprintf("%d_%s", pr.ID, sha)); err != nil {
 | 
			
		||||
		log.Error("Error adding pullID: %d to the pull requests patch checking queue %v", pr.ID, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StartPRCheckAndAutoMerge start an automerge check and auto merge task for a pull request
 | 
			
		||||
func StartPRCheckAndAutoMerge(ctx context.Context, pull *issues_model.PullRequest) {
 | 
			
		||||
	if pull == nil || pull.HasMerged || !pull.CanAutoMerge() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := pull.LoadBaseRepo(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadBaseRepo: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gitRepo, err := gitrepo.OpenRepository(ctx, pull.BaseRepo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("OpenRepository: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer gitRepo.Close()
 | 
			
		||||
	commitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetRefCommitID: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	AddToQueue(pull, commitID)
 | 
			
		||||
}
 | 
			
		||||
@@ -33,8 +33,8 @@ func (p *Pagination) WithCurRows(n int) *Pagination {
 | 
			
		||||
	return p
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pagination) AddParamFromRequest(req *http.Request) {
 | 
			
		||||
	for key, values := range req.URL.Query() {
 | 
			
		||||
func (p *Pagination) AddParamFromQuery(q url.Values) {
 | 
			
		||||
	for key, values := range q {
 | 
			
		||||
		if key == "page" || len(values) == 0 || (len(values) == 1 && values[0] == "") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
@@ -45,6 +45,10 @@ func (p *Pagination) AddParamFromRequest(req *http.Request) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pagination) AddParamFromRequest(req *http.Request) {
 | 
			
		||||
	p.AddParamFromQuery(req.URL.Query())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetParams returns the configured URL params
 | 
			
		||||
func (p *Pagination) GetParams() template.URL {
 | 
			
		||||
	return template.URL(strings.Join(p.urlParams, "&"))
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,6 @@ func init() {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deadline is part of the interface for context.Context and we pass this to the request context
 | 
			
		||||
func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
 | 
			
		||||
	if ctx.Override != nil {
 | 
			
		||||
		return ctx.Override.Deadline()
 | 
			
		||||
@@ -36,7 +35,6 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
 | 
			
		||||
	return ctx.Base.Deadline()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Done is part of the interface for context.Context and we pass this to the request context
 | 
			
		||||
func (ctx *PrivateContext) Done() <-chan struct{} {
 | 
			
		||||
	if ctx.Override != nil {
 | 
			
		||||
		return ctx.Override.Done()
 | 
			
		||||
@@ -44,7 +42,6 @@ func (ctx *PrivateContext) Done() <-chan struct{} {
 | 
			
		||||
	return ctx.Base.Done()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Err is part of the interface for context.Context and we pass this to the request context
 | 
			
		||||
func (ctx *PrivateContext) Err() error {
 | 
			
		||||
	if ctx.Override != nil {
 | 
			
		||||
		return ctx.Override.Err()
 | 
			
		||||
@@ -52,14 +49,14 @@ func (ctx *PrivateContext) Err() error {
 | 
			
		||||
	return ctx.Base.Err()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var privateContextKey any = "default_private_context"
 | 
			
		||||
type privateContextKeyType struct{}
 | 
			
		||||
 | 
			
		||||
var privateContextKey privateContextKeyType
 | 
			
		||||
 | 
			
		||||
// GetPrivateContext returns a context for Private routes
 | 
			
		||||
func GetPrivateContext(req *http.Request) *PrivateContext {
 | 
			
		||||
	return req.Context().Value(privateContextKey).(*PrivateContext)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PrivateContexter returns apicontext as middleware
 | 
			
		||||
func PrivateContexter() func(http.Handler) http.Handler {
 | 
			
		||||
	return func(next http.Handler) http.Handler {
 | 
			
		||||
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
 | 
			
		||||
 
 | 
			
		||||
@@ -143,7 +143,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo
 | 
			
		||||
	mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs)
 | 
			
		||||
	approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs)
 | 
			
		||||
 | 
			
		||||
	teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
 | 
			
		||||
	teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
@@ -485,7 +485,7 @@ func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo
 | 
			
		||||
 | 
			
		||||
	whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs)
 | 
			
		||||
 | 
			
		||||
	teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead)
 | 
			
		||||
	teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -245,7 +245,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
 | 
			
		||||
		RepoTransfer:                  transfer,
 | 
			
		||||
		Topics:                        util.SliceNilAsEmpty(repo.Topics),
 | 
			
		||||
		ObjectFormatName:              repo.ObjectFormatName,
 | 
			
		||||
		Licenses:                      repoLicenses.StringList(),
 | 
			
		||||
		Licenses:                      util.SliceNilAsEmpty(repoLicenses.StringList()),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -171,34 +171,35 @@ func registerDeleteOldSystemNotices() {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GCLFSConfig struct {
 | 
			
		||||
	BaseConfig
 | 
			
		||||
	OlderThan                time.Duration
 | 
			
		||||
	LastUpdatedMoreThanAgo   time.Duration
 | 
			
		||||
	NumberToCheckPerRepo     int64
 | 
			
		||||
	ProportionToCheckPerRepo float64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func registerGCLFS() {
 | 
			
		||||
	if !setting.LFS.StartServer {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	type GCLFSConfig struct {
 | 
			
		||||
		OlderThanConfig
 | 
			
		||||
		LastUpdatedMoreThanAgo   time.Duration
 | 
			
		||||
		NumberToCheckPerRepo     int64
 | 
			
		||||
		ProportionToCheckPerRepo float64
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	RegisterTaskFatal("gc_lfs", &GCLFSConfig{
 | 
			
		||||
		OlderThanConfig: OlderThanConfig{
 | 
			
		||||
			BaseConfig: BaseConfig{
 | 
			
		||||
				Enabled:    false,
 | 
			
		||||
				RunAtStart: false,
 | 
			
		||||
				Schedule:   "@every 24h",
 | 
			
		||||
			},
 | 
			
		||||
			// Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
 | 
			
		||||
			// and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
 | 
			
		||||
			// an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
 | 
			
		||||
			// changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
 | 
			
		||||
			// objects.
 | 
			
		||||
			//
 | 
			
		||||
			// It is likely that a week is potentially excessive but it should definitely be enough that any
 | 
			
		||||
			// unassociated LFS object is genuinely unassociated.
 | 
			
		||||
			OlderThan: 24 * time.Hour * 7,
 | 
			
		||||
		BaseConfig: BaseConfig{
 | 
			
		||||
			Enabled:    false,
 | 
			
		||||
			RunAtStart: false,
 | 
			
		||||
			Schedule:   "@every 24h",
 | 
			
		||||
		},
 | 
			
		||||
		// Only attempt to garbage collect lfs meta objects older than a week as the order of git lfs upload
 | 
			
		||||
		// and git object upload is not necessarily guaranteed. It's possible to imagine a situation whereby
 | 
			
		||||
		// an LFS object is uploaded but the git branch is not uploaded immediately, or there are some rapid
 | 
			
		||||
		// changes in new branches that might lead to lfs objects becoming temporarily unassociated with git
 | 
			
		||||
		// objects.
 | 
			
		||||
		//
 | 
			
		||||
		// It is likely that a week is potentially excessive but it should definitely be enough that any
 | 
			
		||||
		// unassociated LFS object is genuinely unassociated.
 | 
			
		||||
		OlderThan: 24 * time.Hour * 7,
 | 
			
		||||
 | 
			
		||||
		// Only GC things that haven't been looked at in the past 3 days
 | 
			
		||||
		LastUpdatedMoreThanAgo:   24 * time.Hour * 3,
 | 
			
		||||
		NumberToCheckPerRepo:     100,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								services/cron/tasks_extended_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								services/cron/tasks_extended_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
// Copyright 2025 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package cron
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_GCLFSConfig(t *testing.T) {
 | 
			
		||||
	cfg, err := setting.NewConfigProviderFromData(`
 | 
			
		||||
[cron.gc_lfs]
 | 
			
		||||
ENABLED = true
 | 
			
		||||
RUN_AT_START = true
 | 
			
		||||
SCHEDULE = "@every 2h"
 | 
			
		||||
OLDER_THAN = "1h"
 | 
			
		||||
LAST_UPDATED_MORE_THAN_AGO = "7h"
 | 
			
		||||
NUMBER_TO_CHECK_PER_REPO = 10
 | 
			
		||||
PROPORTION_TO_CHECK_PER_REPO = 0.1
 | 
			
		||||
`)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	defer test.MockVariableValue(&setting.CfgProvider, cfg)()
 | 
			
		||||
 | 
			
		||||
	config := &GCLFSConfig{
 | 
			
		||||
		BaseConfig: BaseConfig{
 | 
			
		||||
			Enabled:    false,
 | 
			
		||||
			RunAtStart: false,
 | 
			
		||||
			Schedule:   "@every 24h",
 | 
			
		||||
		},
 | 
			
		||||
		OlderThan:                24 * time.Hour * 7,
 | 
			
		||||
		LastUpdatedMoreThanAgo:   24 * time.Hour * 3,
 | 
			
		||||
		NumberToCheckPerRepo:     100,
 | 
			
		||||
		ProportionToCheckPerRepo: 0.6,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = setting.GetCronSettings("gc_lfs", config)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.True(t, config.Enabled)
 | 
			
		||||
	assert.True(t, config.RunAtStart)
 | 
			
		||||
	assert.Equal(t, "@every 2h", config.Schedule)
 | 
			
		||||
	assert.Equal(t, 1*time.Hour, config.OlderThan)
 | 
			
		||||
	assert.Equal(t, 7*time.Hour, config.LastUpdatedMoreThanAgo)
 | 
			
		||||
	assert.Equal(t, int64(10), config.NumberToCheckPerRepo)
 | 
			
		||||
	assert.InDelta(t, 0.1, config.ProportionToCheckPerRepo, 0.001)
 | 
			
		||||
}
 | 
			
		||||
@@ -179,7 +179,7 @@ func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
 | 
			
		||||
	leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line)
 | 
			
		||||
	leftLine, leftHunk, rightLine, rightHunk := git.ParseDiffHunkString(line)
 | 
			
		||||
 | 
			
		||||
	return &DiffLineSectionInfo{
 | 
			
		||||
		Path:          treePath,
 | 
			
		||||
@@ -188,7 +188,7 @@ func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int
 | 
			
		||||
		LeftIdx:       leftLine,
 | 
			
		||||
		RightIdx:      rightLine,
 | 
			
		||||
		LeftHunkSize:  leftHunk,
 | 
			
		||||
		RightHunkSize: righHunk,
 | 
			
		||||
		RightHunkSize: rightHunk,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -335,7 +335,7 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc
 | 
			
		||||
	// try to find equivalent diff line. ignore, otherwise
 | 
			
		||||
	switch diffLine.Type {
 | 
			
		||||
	case DiffLineSection:
 | 
			
		||||
		return getLineContent(diffLine.Content[1:], locale)
 | 
			
		||||
		return getLineContent(diffLine.Content, locale)
 | 
			
		||||
	case DiffLineAdd:
 | 
			
		||||
		compareDiffLine := diffSection.GetLine(diffLine.Match)
 | 
			
		||||
		return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale)
 | 
			
		||||
@@ -904,6 +904,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
 | 
			
		||||
			lastLeftIdx = -1
 | 
			
		||||
			curFile.Sections = append(curFile.Sections, curSection)
 | 
			
		||||
 | 
			
		||||
			// FIXME: the "-1" can't be right, these "line idx" are all 1-based, maybe there are other bugs that covers this bug.
 | 
			
		||||
			lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
 | 
			
		||||
			diffLine := &DiffLine{
 | 
			
		||||
				Type:        DiffLineSection,
 | 
			
		||||
@@ -1232,13 +1233,13 @@ func GetDiffForAPI(ctx context.Context, gitRepo *git.Repository, opts *DiffOptio
 | 
			
		||||
	return diff, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
 | 
			
		||||
func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
 | 
			
		||||
	diff, beforeCommit, afterCommit, err := getDiffBasic(ctx, gitRepo, opts, files...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage})
 | 
			
		||||
	checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -1247,6 +1248,7 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
 | 
			
		||||
	for _, diffFile := range diff.Files {
 | 
			
		||||
		isVendored := optional.None[bool]()
 | 
			
		||||
		isGenerated := optional.None[bool]()
 | 
			
		||||
		attrDiff := optional.None[string]()
 | 
			
		||||
		attrs, err := checker.CheckPath(diffFile.Name)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated()
 | 
			
		||||
@@ -1254,11 +1256,12 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
 | 
			
		||||
			if language.Has() {
 | 
			
		||||
				diffFile.Language = language.Value()
 | 
			
		||||
			}
 | 
			
		||||
			attrDiff = attrs.Get(attribute.Diff).ToString()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Populate Submodule URLs
 | 
			
		||||
		if diffFile.SubmoduleDiffInfo != nil {
 | 
			
		||||
			diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, afterCommit)
 | 
			
		||||
			diffFile.SubmoduleDiffInfo.PopulateURL(repoLink, diffFile, beforeCommit, afterCommit)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !isVendored.Has() {
 | 
			
		||||
@@ -1275,7 +1278,8 @@ func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOp
 | 
			
		||||
			diffFile.Sections = append(diffFile.Sections, tailSection)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !setting.Git.DisableDiffHighlight {
 | 
			
		||||
		shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
 | 
			
		||||
		if shouldFullFileHighlight {
 | 
			
		||||
			if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
 | 
			
		||||
				diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String())
 | 
			
		||||
			}
 | 
			
		||||
@@ -1359,6 +1363,7 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.
 | 
			
		||||
	// But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err)
 | 
			
		||||
		err = nil //nolint
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState)
 | 
			
		||||
@@ -1400,7 +1405,7 @@ outer:
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return review, err
 | 
			
		||||
	return review, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CommentAsDiff returns c.Patch as *Diff
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ type SubmoduleDiffInfo struct {
 | 
			
		||||
	PreviousRefID string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCommit *git.Commit) {
 | 
			
		||||
func (si *SubmoduleDiffInfo) PopulateURL(repoLink string, diffFile *DiffFile, leftCommit, rightCommit *git.Commit) {
 | 
			
		||||
	si.SubmoduleName = diffFile.Name
 | 
			
		||||
	submoduleCommit := rightCommit // If the submodule is added or updated, check at the right commit
 | 
			
		||||
	if diffFile.IsDeleted {
 | 
			
		||||
@@ -30,18 +30,19 @@ func (si *SubmoduleDiffInfo) PopulateURL(diffFile *DiffFile, leftCommit, rightCo
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	submodule, err := submoduleCommit.GetSubModule(diffFile.GetDiffFileName())
 | 
			
		||||
	submoduleFullPath := diffFile.GetDiffFileName()
 | 
			
		||||
	submodule, err := submoduleCommit.GetSubModule(submoduleFullPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", diffFile.GetDiffFileName(), err)
 | 
			
		||||
		log.Error("Unable to PopulateURL for submodule %q: GetSubModule: %v", submoduleFullPath, err)
 | 
			
		||||
		return // ignore the error, do not cause 500 errors for end users
 | 
			
		||||
	}
 | 
			
		||||
	if submodule != nil {
 | 
			
		||||
		si.SubmoduleFile = git.NewCommitSubmoduleFile(submodule.URL, submoduleCommit.ID.String())
 | 
			
		||||
		si.SubmoduleFile = git.NewCommitSubmoduleFile(repoLink, submoduleFullPath, submodule.URL, submoduleCommit.ID.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID string) template.HTML {
 | 
			
		||||
	webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, commitID)
 | 
			
		||||
	webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx, commitID)
 | 
			
		||||
	if webLink == nil {
 | 
			
		||||
		return htmlutil.HTMLFormat("%s", base.ShortSha(commitID))
 | 
			
		||||
	}
 | 
			
		||||
@@ -49,7 +50,7 @@ func (si *SubmoduleDiffInfo) CommitRefIDLinkHTML(ctx context.Context, commitID s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.HTML {
 | 
			
		||||
	webLink := si.SubmoduleFile.SubmoduleWebLink(ctx, si.PreviousRefID, si.NewRefID)
 | 
			
		||||
	webLink := si.SubmoduleFile.SubmoduleWebLinkCompare(ctx, si.PreviousRefID, si.NewRefID)
 | 
			
		||||
	if webLink == nil {
 | 
			
		||||
		return htmlutil.HTMLFormat("%s...%s", base.ShortSha(si.PreviousRefID), base.ShortSha(si.NewRefID))
 | 
			
		||||
	}
 | 
			
		||||
@@ -57,7 +58,7 @@ func (si *SubmoduleDiffInfo) CompareRefIDLinkHTML(ctx context.Context) template.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (si *SubmoduleDiffInfo) SubmoduleRepoLinkHTML(ctx context.Context) template.HTML {
 | 
			
		||||
	webLink := si.SubmoduleFile.SubmoduleWebLink(ctx)
 | 
			
		||||
	webLink := si.SubmoduleFile.SubmoduleWebLinkTree(ctx)
 | 
			
		||||
	if webLink == nil {
 | 
			
		||||
		return htmlutil.HTMLFormat("%s", si.SubmoduleName)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -228,7 +228,7 @@ func TestSubmoduleInfo(t *testing.T) {
 | 
			
		||||
	assert.EqualValues(t, "aaaa...bbbb", sdi.CompareRefIDLinkHTML(ctx))
 | 
			
		||||
	assert.EqualValues(t, "name", sdi.SubmoduleRepoLinkHTML(ctx))
 | 
			
		||||
 | 
			
		||||
	sdi.SubmoduleFile = git.NewCommitSubmoduleFile("https://github.com/owner/repo", "1234")
 | 
			
		||||
	sdi.SubmoduleFile = git.NewCommitSubmoduleFile("/any/repo-link", "fullpath", "https://github.com/owner/repo", "1234")
 | 
			
		||||
	assert.EqualValues(t, `<a href="https://github.com/owner/repo/tree/1111">1111</a>`, sdi.CommitRefIDLinkHTML(ctx, "1111"))
 | 
			
		||||
	assert.EqualValues(t, `<a href="https://github.com/owner/repo/compare/aaaa...bbbb">aaaa...bbbb</a>`, sdi.CompareRefIDLinkHTML(ctx))
 | 
			
		||||
	assert.EqualValues(t, `<a href="https://github.com/owner/repo">name</a>`, sdi.SubmoduleRepoLinkHTML(ctx))
 | 
			
		||||
 
 | 
			
		||||
@@ -304,7 +304,7 @@ func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, rep
 | 
			
		||||
 | 
			
		||||
	// If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers
 | 
			
		||||
	if repo.Owner.IsOrganization() {
 | 
			
		||||
		teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead)
 | 
			
		||||
		teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("GetTeamsWithAccessToRepo: %v", err)
 | 
			
		||||
			return false
 | 
			
		||||
 
 | 
			
		||||
@@ -80,6 +80,12 @@ func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_m
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// reload issue to ensure it has the latest data, especially the number of comments
 | 
			
		||||
	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	notify_service.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
 | 
			
		||||
 | 
			
		||||
	return comment, nil
 | 
			
		||||
 
 | 
			
		||||
@@ -322,7 +322,10 @@ func (g *GithubDownloaderV3) convertGithubRelease(ctx context.Context, rel *gith
 | 
			
		||||
	httpClient := NewMigrationHTTPClient()
 | 
			
		||||
 | 
			
		||||
	for _, asset := range rel.Assets {
 | 
			
		||||
		assetID := *asset.ID // Don't optimize this, for closure we need a local variable
 | 
			
		||||
		assetID := asset.GetID() // Don't optimize this, for closure we need a local variable TODO: no need to do so in new Golang
 | 
			
		||||
		if assetID == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		r.Assets = append(r.Assets, &base.ReleaseAsset{
 | 
			
		||||
			ID:            asset.GetID(),
 | 
			
		||||
			Name:          asset.GetName(),
 | 
			
		||||
 
 | 
			
		||||
@@ -46,10 +46,25 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func shouldSendCommentChangeNotification(ctx context.Context, comment *issues_model.Comment) bool {
 | 
			
		||||
	if err := comment.LoadReview(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadReview: %v", err)
 | 
			
		||||
		return false
 | 
			
		||||
	} else if comment.Review != nil && comment.Review.Type == issues_model.ReviewTypePending {
 | 
			
		||||
		// Pending review comments updating should not triggered
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateIssueComment notifies issue comment related message to notifiers
 | 
			
		||||
func CreateIssueComment(ctx context.Context, doer *user_model.User, repo *repo_model.Repository,
 | 
			
		||||
	issue *issues_model.Issue, comment *issues_model.Comment, mentions []*user_model.User,
 | 
			
		||||
) {
 | 
			
		||||
	if !shouldSendCommentChangeNotification(ctx, comment) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, notifier := range notifiers {
 | 
			
		||||
		notifier.CreateIssueComment(ctx, doer, repo, issue, comment, mentions)
 | 
			
		||||
	}
 | 
			
		||||
@@ -156,6 +171,10 @@ func PullReviewDismiss(ctx context.Context, doer *user_model.User, review *issue
 | 
			
		||||
 | 
			
		||||
// UpdateComment notifies update comment to notifiers
 | 
			
		||||
func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment, oldContent string) {
 | 
			
		||||
	if !shouldSendCommentChangeNotification(ctx, c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, notifier := range notifiers {
 | 
			
		||||
		notifier.UpdateComment(ctx, doer, c, oldContent)
 | 
			
		||||
	}
 | 
			
		||||
@@ -163,6 +182,10 @@ func UpdateComment(ctx context.Context, doer *user_model.User, c *issues_model.C
 | 
			
		||||
 | 
			
		||||
// DeleteComment notifies delete comment to notifiers
 | 
			
		||||
func DeleteComment(ctx context.Context, doer *user_model.User, c *issues_model.Comment) {
 | 
			
		||||
	if !shouldSendCommentChangeNotification(ctx, c) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, notifier := range notifiers {
 | 
			
		||||
		notifier.DeleteComment(ctx, doer, c)
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
// Copyright 2019 The Gitea Authors.
 | 
			
		||||
// All rights reserved.
 | 
			
		||||
// Copyright 2019 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package pull
 | 
			
		||||
@@ -16,6 +15,7 @@ import (
 | 
			
		||||
	git_model "code.gitea.io/gitea/models/git"
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	access_model "code.gitea.io/gitea/models/perm/access"
 | 
			
		||||
	"code.gitea.io/gitea/models/pull"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unit"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
@@ -29,6 +29,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
	asymkey_service "code.gitea.io/gitea/services/asymkey"
 | 
			
		||||
	"code.gitea.io/gitea/services/automergequeue"
 | 
			
		||||
	notify_service "code.gitea.io/gitea/services/notify"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -238,7 +239,7 @@ func isSignedIfRequired(ctx context.Context, pr *issues_model.PullRequest, doer
 | 
			
		||||
// markPullRequestAsMergeable checks if pull request is possible to leaving checking status,
 | 
			
		||||
// and set to be either conflict or mergeable.
 | 
			
		||||
func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullRequest) {
 | 
			
		||||
	// If status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
 | 
			
		||||
	// If the status has not been changed to conflict by testPullRequestTmpRepoBranchMergeable then we are mergeable
 | 
			
		||||
	if pr.Status == issues_model.PullRequestStatusChecking {
 | 
			
		||||
		pr.Status = issues_model.PullRequestStatusMergeable
 | 
			
		||||
	}
 | 
			
		||||
@@ -257,6 +258,16 @@ func markPullRequestAsMergeable(ctx context.Context, pr *issues_model.PullReques
 | 
			
		||||
	if err := pr.UpdateColsIfNotMerged(ctx, "merge_base", "status", "conflicted_files", "changed_protected_files"); err != nil {
 | 
			
		||||
		log.Error("Update[%-v]: %v", pr, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if there is a scheduled merge for this pull request, start the auto merge check (again)
 | 
			
		||||
	exist, _, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetScheduledMergeByPullID[%-v]: %v", pr, err)
 | 
			
		||||
		return
 | 
			
		||||
	} else if !exist {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	automergequeue.StartPRCheckAndAutoMerge(ctx, pr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getMergeCommit checks if a pull request has been merged
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
// Copyright 2019 The Gitea Authors.
 | 
			
		||||
// All rights reserved.
 | 
			
		||||
// Copyright 2019 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package pull
 | 
			
		||||
@@ -11,11 +10,18 @@ import (
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models/db"
 | 
			
		||||
	issues_model "code.gitea.io/gitea/models/issues"
 | 
			
		||||
	"code.gitea.io/gitea/models/pull"
 | 
			
		||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
			
		||||
	"code.gitea.io/gitea/models/unittest"
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/graceful"
 | 
			
		||||
	"code.gitea.io/gitea/modules/queue"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/test"
 | 
			
		||||
	"code.gitea.io/gitea/services/automergequeue"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestPullRequest_AddToTaskQueue(t *testing.T) {
 | 
			
		||||
@@ -63,6 +69,46 @@ func TestPullRequest_AddToTaskQueue(t *testing.T) {
 | 
			
		||||
	pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
 | 
			
		||||
	assert.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
 | 
			
		||||
 | 
			
		||||
	prPatchCheckerQueue.ShutdownWait(5 * time.Second)
 | 
			
		||||
	prPatchCheckerQueue.ShutdownWait(time.Second)
 | 
			
		||||
	prPatchCheckerQueue = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMarkPullRequestAsMergeable(t *testing.T) {
 | 
			
		||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
			
		||||
 | 
			
		||||
	prPatchCheckerQueue = queue.CreateUniqueQueue(graceful.GetManager().ShutdownContext(), "pr_patch_checker", func(items ...string) []string { return nil })
 | 
			
		||||
	go prPatchCheckerQueue.Run()
 | 
			
		||||
	defer func() {
 | 
			
		||||
		prPatchCheckerQueue.ShutdownWait(time.Second)
 | 
			
		||||
		prPatchCheckerQueue = nil
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	addToQueueShaChan := make(chan string, 1)
 | 
			
		||||
	defer test.MockVariableValue(&automergequeue.AddToQueue, func(pr *issues_model.PullRequest, sha string) {
 | 
			
		||||
		addToQueueShaChan <- sha
 | 
			
		||||
	})()
 | 
			
		||||
	ctx := t.Context()
 | 
			
		||||
	_, _ = db.GetEngine(ctx).ID(2).Update(&issues_model.PullRequest{Status: issues_model.PullRequestStatusChecking})
 | 
			
		||||
	pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
 | 
			
		||||
	require.False(t, pr.HasMerged)
 | 
			
		||||
	require.Equal(t, issues_model.PullRequestStatusChecking, pr.Status)
 | 
			
		||||
 | 
			
		||||
	err := pull.ScheduleAutoMerge(ctx, &user_model.User{ID: 99999}, pr.ID, repo_model.MergeStyleMerge, "test msg", true)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	exist, scheduleMerge, err := pull.GetScheduledMergeByPullID(ctx, pr.ID)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	assert.True(t, exist)
 | 
			
		||||
	assert.True(t, scheduleMerge.Doer.IsGhost())
 | 
			
		||||
 | 
			
		||||
	markPullRequestAsMergeable(ctx, pr)
 | 
			
		||||
	pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
 | 
			
		||||
	require.Equal(t, issues_model.PullRequestStatusMergeable, pr.Status)
 | 
			
		||||
 | 
			
		||||
	select {
 | 
			
		||||
	case sha := <-addToQueueShaChan:
 | 
			
		||||
		assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", sha) // ref: refs/pull/3/head
 | 
			
		||||
	case <-time.After(1 * time.Second):
 | 
			
		||||
		assert.FailNow(t, "Timeout: nothing was added to automergequeue")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,15 +35,16 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, gp := range requiredContextsGlob {
 | 
			
		||||
			var targetStatus structs.CommitStatusState
 | 
			
		||||
			var targetStatuses []*git_model.CommitStatus
 | 
			
		||||
			for _, commitStatus := range commitStatuses {
 | 
			
		||||
				if gp.Match(commitStatus.Context) {
 | 
			
		||||
					targetStatus = commitStatus.State
 | 
			
		||||
					targetStatuses = append(targetStatuses, commitStatus)
 | 
			
		||||
					matchedCount++
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			targetStatus := git_model.CalcCommitStatus(targetStatuses).State
 | 
			
		||||
 | 
			
		||||
			// If required rule not match any action, then it is pending
 | 
			
		||||
			if targetStatus == "" {
 | 
			
		||||
				if structs.CommitStatusPending.NoBetterThan(returnedStatus) {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,11 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
 | 
			
		||||
			{Context: "Build 2", State: structs.CommitStatusSuccess},
 | 
			
		||||
			{Context: "Build 2t", State: structs.CommitStatusFailure},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			{Context: "Build 1", State: structs.CommitStatusSuccess},
 | 
			
		||||
			{Context: "Build 2", State: structs.CommitStatusSuccess},
 | 
			
		||||
			{Context: "Build 2t", State: structs.CommitStatusFailure},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			{Context: "Build 1", State: structs.CommitStatusSuccess},
 | 
			
		||||
			{Context: "Build 2", State: structs.CommitStatusSuccess},
 | 
			
		||||
@@ -45,6 +50,7 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
 | 
			
		||||
		{"Build*"},
 | 
			
		||||
		{"Build*", "Build 2t*"},
 | 
			
		||||
		{"Build*", "Build 2t*"},
 | 
			
		||||
		{"Build*"},
 | 
			
		||||
		{"Build*", "Build 2t*", "Build 3*"},
 | 
			
		||||
		{"Build*", "Build *", "Build 2t*", "Build 1*"},
 | 
			
		||||
	}
 | 
			
		||||
@@ -53,6 +59,7 @@ func TestMergeRequiredContextsCommitStatus(t *testing.T) {
 | 
			
		||||
		structs.CommitStatusSuccess,
 | 
			
		||||
		structs.CommitStatusPending,
 | 
			
		||||
		structs.CommitStatusFailure,
 | 
			
		||||
		structs.CommitStatusFailure,
 | 
			
		||||
		structs.CommitStatusPending,
 | 
			
		||||
		structs.CommitStatusSuccess,
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -85,5 +85,5 @@ func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*orga
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
	return organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -90,15 +90,8 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git
 | 
			
		||||
	if rangeStart >= len(entries) {
 | 
			
		||||
		return tree, nil
 | 
			
		||||
	}
 | 
			
		||||
	var rangeEnd int
 | 
			
		||||
	if len(entries) > perPage {
 | 
			
		||||
		tree.Truncated = true
 | 
			
		||||
	}
 | 
			
		||||
	if rangeStart+perPage < len(entries) {
 | 
			
		||||
		rangeEnd = rangeStart + perPage
 | 
			
		||||
	} else {
 | 
			
		||||
		rangeEnd = len(entries)
 | 
			
		||||
	}
 | 
			
		||||
	rangeEnd := min(rangeStart+perPage, len(entries))
 | 
			
		||||
	tree.Truncated = rangeEnd < len(entries)
 | 
			
		||||
	tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart)
 | 
			
		||||
	for e := rangeStart; e < rangeEnd; e++ {
 | 
			
		||||
		i := e - rangeStart
 | 
			
		||||
@@ -158,35 +151,29 @@ func (node *TreeViewNode) sortLevel() int {
 | 
			
		||||
	return util.Iif(node.EntryMode == "tree" || node.EntryMode == "commit", 0, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
 | 
			
		||||
func newTreeViewNodeFromEntry(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, parentDir string, entry *git.TreeEntry) *TreeViewNode {
 | 
			
		||||
	node := &TreeViewNode{
 | 
			
		||||
		EntryName: entry.Name(),
 | 
			
		||||
		EntryMode: entryModeString(entry.Mode()),
 | 
			
		||||
		FullPath:  path.Join(parentDir, entry.Name()),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if entry.IsLink() {
 | 
			
		||||
		// TODO: symlink to a folder or a file, the icon differs
 | 
			
		||||
		target, err := entry.FollowLink()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			_ = target.IsDir()
 | 
			
		||||
			// if target.IsDir() { } else { }
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if node.EntryIcon == "" {
 | 
			
		||||
		node.EntryIcon = fileicon.RenderEntryIcon(renderedIconPool, entry)
 | 
			
		||||
		// TODO: no open icon support yet
 | 
			
		||||
		// node.EntryIconOpen = fileicon.RenderEntryIconOpen(renderedIconPool, entry)
 | 
			
		||||
	entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry)
 | 
			
		||||
	node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
 | 
			
		||||
	if entryInfo.EntryMode.IsDir() {
 | 
			
		||||
		entryInfo.IsOpen = true
 | 
			
		||||
		node.EntryIconOpen = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if node.EntryMode == "commit" {
 | 
			
		||||
		if subModule, err := commit.GetSubModule(node.FullPath); err != nil {
 | 
			
		||||
			log.Error("GetSubModule: %v", err)
 | 
			
		||||
		} else if subModule != nil {
 | 
			
		||||
			submoduleFile := git.NewCommitSubmoduleFile(subModule.URL, entry.ID.String())
 | 
			
		||||
			webLink := submoduleFile.SubmoduleWebLink(ctx)
 | 
			
		||||
			node.SubmoduleURL = webLink.CommitWebLink
 | 
			
		||||
			submoduleFile := git.NewCommitSubmoduleFile(repoLink, node.FullPath, subModule.URL, entry.ID.String())
 | 
			
		||||
			webLink := submoduleFile.SubmoduleWebLinkTree(ctx)
 | 
			
		||||
			if webLink != nil {
 | 
			
		||||
				node.SubmoduleURL = webLink.CommitWebLink
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -204,7 +191,7 @@ func sortTreeViewNodes(nodes []*TreeViewNode) {
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
 | 
			
		||||
func listTreeNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, tree *git.Tree, treePath, subPath string) ([]*TreeViewNode, error) {
 | 
			
		||||
	entries, err := tree.ListEntries()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
@@ -213,14 +200,14 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP
 | 
			
		||||
	subPathDirName, subPathRemaining, _ := strings.Cut(subPath, "/")
 | 
			
		||||
	nodes := make([]*TreeViewNode, 0, len(entries))
 | 
			
		||||
	for _, entry := range entries {
 | 
			
		||||
		node := newTreeViewNodeFromEntry(ctx, renderedIconPool, commit, treePath, entry)
 | 
			
		||||
		node := newTreeViewNodeFromEntry(ctx, repoLink, renderedIconPool, commit, treePath, entry)
 | 
			
		||||
		nodes = append(nodes, node)
 | 
			
		||||
		if entry.IsDir() && subPathDirName == entry.Name() {
 | 
			
		||||
			subTreePath := treePath + "/" + node.EntryName
 | 
			
		||||
			if subTreePath[0] == '/' {
 | 
			
		||||
				subTreePath = subTreePath[1:]
 | 
			
		||||
			}
 | 
			
		||||
			subNodes, err := listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
 | 
			
		||||
			subNodes, err := listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), subTreePath, subPathRemaining)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("listTreeNodes: %v", err)
 | 
			
		||||
			} else {
 | 
			
		||||
@@ -232,10 +219,10 @@ func listTreeNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconP
 | 
			
		||||
	return nodes, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetTreeViewNodes(ctx context.Context, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
 | 
			
		||||
func GetTreeViewNodes(ctx context.Context, repoLink string, renderedIconPool *fileicon.RenderedIconPool, commit *git.Commit, treePath, subPath string) ([]*TreeViewNode, error) {
 | 
			
		||||
	entry, err := commit.GetTreeEntryByPath(treePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return listTreeNodes(ctx, renderedIconPool, commit, entry.Tree(), treePath, subPath)
 | 
			
		||||
	return listTreeNodes(ctx, repoLink, renderedIconPool, commit, entry.Tree(), treePath, subPath)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ func TestGetTreeViewNodes(t *testing.T) {
 | 
			
		||||
	contexttest.LoadGitRepo(t, ctx)
 | 
			
		||||
	defer ctx.Repo.GitRepo.Close()
 | 
			
		||||
 | 
			
		||||
	curRepoLink := "/any/repo-link"
 | 
			
		||||
	renderedIconPool := fileicon.NewRenderedIconPool()
 | 
			
		||||
	mockIconForFile := func(id string) template.HTML {
 | 
			
		||||
		return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
 | 
			
		||||
@@ -71,25 +72,30 @@ func TestGetTreeViewNodes(t *testing.T) {
 | 
			
		||||
	mockIconForFolder := func(id string) template.HTML {
 | 
			
		||||
		return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
 | 
			
		||||
	}
 | 
			
		||||
	treeNodes, err := GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "")
 | 
			
		||||
	mockOpenIconForFolder := func(id string) template.HTML {
 | 
			
		||||
		return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use xlink:href="#` + id + `"></use></svg>`)
 | 
			
		||||
	}
 | 
			
		||||
	treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, []*TreeViewNode{
 | 
			
		||||
		{
 | 
			
		||||
			EntryName: "docs",
 | 
			
		||||
			EntryMode: "tree",
 | 
			
		||||
			FullPath:  "docs",
 | 
			
		||||
			EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
 | 
			
		||||
			EntryName:     "docs",
 | 
			
		||||
			EntryMode:     "tree",
 | 
			
		||||
			FullPath:      "docs",
 | 
			
		||||
			EntryIcon:     mockIconForFolder(`svg-mfi-folder-docs`),
 | 
			
		||||
			EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
 | 
			
		||||
		},
 | 
			
		||||
	}, treeNodes)
 | 
			
		||||
 | 
			
		||||
	treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
 | 
			
		||||
	treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "docs/README.md")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, []*TreeViewNode{
 | 
			
		||||
		{
 | 
			
		||||
			EntryName: "docs",
 | 
			
		||||
			EntryMode: "tree",
 | 
			
		||||
			FullPath:  "docs",
 | 
			
		||||
			EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
 | 
			
		||||
			EntryName:     "docs",
 | 
			
		||||
			EntryMode:     "tree",
 | 
			
		||||
			FullPath:      "docs",
 | 
			
		||||
			EntryIcon:     mockIconForFolder(`svg-mfi-folder-docs`),
 | 
			
		||||
			EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
 | 
			
		||||
			Children: []*TreeViewNode{
 | 
			
		||||
				{
 | 
			
		||||
					EntryName: "README.md",
 | 
			
		||||
@@ -101,7 +107,7 @@ func TestGetTreeViewNodes(t *testing.T) {
 | 
			
		||||
		},
 | 
			
		||||
	}, treeNodes)
 | 
			
		||||
 | 
			
		||||
	treeNodes, err = GetTreeViewNodes(ctx, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
 | 
			
		||||
	treeNodes, err = GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "docs", "README.md")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	assert.Equal(t, []*TreeViewNode{
 | 
			
		||||
		{
 | 
			
		||||
 
 | 
			
		||||
@@ -402,16 +402,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rel, has := relMap[lowerTag]
 | 
			
		||||
 | 
			
		||||
		parts := strings.SplitN(tag.Message, "\n", 2)
 | 
			
		||||
		note := ""
 | 
			
		||||
		if len(parts) > 1 {
 | 
			
		||||
			note = parts[1]
 | 
			
		||||
		}
 | 
			
		||||
		title, note := git.SplitCommitTitleBody(tag.Message, 255)
 | 
			
		||||
		if !has {
 | 
			
		||||
			rel = &repo_model.Release{
 | 
			
		||||
				RepoID:       repo.ID,
 | 
			
		||||
				Title:        parts[0],
 | 
			
		||||
				Title:        title,
 | 
			
		||||
				TagName:      tags[i],
 | 
			
		||||
				LowerTagName: lowerTag,
 | 
			
		||||
				Target:       "",
 | 
			
		||||
@@ -430,7 +425,7 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
 | 
			
		||||
			rel.Sha1 = commit.ID.String()
 | 
			
		||||
			rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
 | 
			
		||||
			if rel.IsTag {
 | 
			
		||||
				rel.Title = parts[0]
 | 
			
		||||
				rel.Title = title
 | 
			
		||||
				rel.Note = note
 | 
			
		||||
			} else {
 | 
			
		||||
				rel.IsDraft = false
 | 
			
		||||
 
 | 
			
		||||
@@ -445,6 +445,7 @@ func (m *webhookNotifier) DeleteComment(ctx context.Context, doer *user_model.Us
 | 
			
		||||
		log.Error("LoadPoster: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	comment.Issue = nil // reload issue to ensure it has the latest data, especially the number of comments
 | 
			
		||||
	if err = comment.LoadIssue(ctx); err != nil {
 | 
			
		||||
		log.Error("LoadIssue: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
					<td class="author">
 | 
			
		||||
						<div class="tw-flex">
 | 
			
		||||
							{{$userName := .Author.Name}}
 | 
			
		||||
							{{if .User}}
 | 
			
		||||
							{{if and .User (gt .User.ID 0)}}
 | 
			
		||||
								{{if and .User.FullName DefaultShowFullName}}
 | 
			
		||||
									{{$userName = .User.FullName}}
 | 
			
		||||
								{{end}}
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user