diff --git a/Makefile b/Makefile index 34685dea93..69eb088ba7 100644 --- a/Makefile +++ b/Makefile @@ -438,9 +438,9 @@ js: npm .PHONY: css css: npm - npx stylelint public/less - npx lessc --clean-css="--s0 -b" public/less/index.less public/css/index.css - $(foreach file, $(filter-out public/less/themes/_base.less, $(wildcard public/less/themes/*)),npx lessc --clean-css="--s0 -b" public/less/themes/$(notdir $(file)) > public/css/theme-$(notdir $(call strip-suffix,$(file))).css;) + npx stylelint web_src/less + npx lessc --clean-css="--s0 -b" web_src/less/index.less public/css/index.css + $(foreach file, $(filter-out web_src/less/themes/_base.less, $(wildcard web_src/less/themes/*)),npx lessc --clean-css="--s0 -b" web_src/less/themes/$(notdir $(file)) > public/css/theme-$(notdir $(call strip-suffix,$(file))).css;) npx postcss --use autoprefixer --no-map --replace public/css/* @diff=$$(git diff public/css/*); \ diff --git a/cmd/admin.go b/cmd/admin.go index 83ced76c96..1e41a0af6c 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -375,17 +375,20 @@ func runRepoSyncReleases(c *cli.Context) error { if err = models.SyncReleasesWithTags(repo, gitRepo); err != nil { log.Warn(" SyncReleasesWithTags: %v", err) + gitRepo.Close() continue } count, err = getReleaseCount(repo.ID) if err != nil { log.Warn(" GetReleaseCountByRepoID: %v", err) + gitRepo.Close() continue } log.Trace(" repo %s releases synchronized to tags: from %d to %d", repo.FullName(), oldnum, count) + gitRepo.Close() } } diff --git a/contrib/systemd/gitea.service b/contrib/systemd/gitea.service index a69588893b..73e838c23b 100644 --- a/contrib/systemd/gitea.service +++ b/contrib/systemd/gitea.service @@ -51,8 +51,8 @@ Type=simple User=git Group=git WorkingDirectory=/var/lib/gitea/ -# If using unix socket: Tells Systemd to create /run/gitea folder to home gitea.sock -# Manual cration would vanish after reboot. +# If using Unix socket: tells systemd to create the /run/gitea folder, which will contain the gitea.sock file +# (manually creating /run/gitea doesn't work, because it would not persist across reboots) #RuntimeDirectory=gitea ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini Restart=always diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample index 17fcc0de23..5e26171d9e 100644 --- a/custom/conf/app.ini.sample +++ b/custom/conf/app.ini.sample @@ -501,6 +501,9 @@ SHOW_REGISTRATION_BUTTON = true ; When adding a repo to a team or creating a new repo all team members will watch the ; repo automatically if enabled AUTO_WATCH_NEW_REPOS = true +; Default value for AutoWatchOnChanges +; Make the user watch a repository When they commit for the first time +AUTO_WATCH_ON_CHANGES = false [webhook] ; Hook task queue length, increase if webhook shooting starts hanging diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 96b529c0bc..e5236205fe 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -303,6 +303,7 @@ relation to port exhaustion. on this instance. - `SHOW_REGISTRATION_BUTTON`: **! DISABLE\_REGISTRATION**: Show Registration Button - `AUTO_WATCH_NEW_REPOS`: **true**: Enable this to let all organisation users watch new repos when they are created +- `AUTO_WATCH_ON_CHANGES`: **false**: Enable this to make users watch a repository after their first commit to it - `DEFAULT_ORG_VISIBILITY`: **public**: Set default visibility mode for organisations, either "public", "limited" or "private". - `DEFAULT_ORG_MEMBER_VISIBLE`: **false** True will make the membership of the users visible when added to the organisation. diff --git a/docs/content/doc/advanced/migrations.en-us.md b/docs/content/doc/advanced/migrations.en-us.md index 2511f7af89..0d9d8b49a7 100644 --- a/docs/content/doc/advanced/migrations.en-us.md +++ b/docs/content/doc/advanced/migrations.en-us.md @@ -68,6 +68,7 @@ type Uploader interface { CreateComment(issueNumber int64, comment *Comment) error CreatePullRequest(pr *PullRequest) error Rollback() error + Close() } -``` \ No newline at end of file +``` diff --git a/docs/content/doc/usage/fail2ban-setup.md b/docs/content/doc/usage/fail2ban-setup.md index 922c71f93d..59f4db2f11 100644 --- a/docs/content/doc/usage/fail2ban-setup.md +++ b/docs/content/doc/usage/fail2ban-setup.md @@ -13,7 +13,7 @@ menu: identifier: "fail2ban-setup" --- -# Fail2ban setup to block users after failed login attemts +# Fail2ban setup to block users after failed login attempts **Remember that fail2ban is powerful and can cause lots of issues if you do it incorrectly, so make sure to test this before relying on it so you don't lock yourself out.** diff --git a/integrations/api_releases_test.go b/integrations/api_releases_test.go index 897f863eb3..8025f1de5d 100644 --- a/integrations/api_releases_test.go +++ b/integrations/api_releases_test.go @@ -51,6 +51,7 @@ func TestAPICreateAndUpdateRelease(t *testing.T) { gitRepo, err := git.OpenRepository(repo.RepoPath()) assert.NoError(t, err) + defer gitRepo.Close() err = gitRepo.CreateTag("v0.0.1", "master") assert.NoError(t, err) @@ -112,6 +113,7 @@ func TestAPICreateReleaseToDefaultBranchOnExistingTag(t *testing.T) { gitRepo, err := git.OpenRepository(repo.RepoPath()) assert.NoError(t, err) + defer gitRepo.Close() err = gitRepo.CreateTag("v0.0.1", "master") assert.NoError(t, err) diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go index 4d76ff00ce..eb437edf03 100644 --- a/integrations/api_repo_file_create_test.go +++ b/integrations/api_repo_file_create_test.go @@ -139,6 +139,7 @@ func TestAPICreateFile(t *testing.T) { assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) + gitRepo.Close() } // Test creating a file in a new branch diff --git a/integrations/api_repo_file_update_test.go b/integrations/api_repo_file_update_test.go index bf695d4344..236cb8eb30 100644 --- a/integrations/api_repo_file_update_test.go +++ b/integrations/api_repo_file_update_test.go @@ -143,6 +143,7 @@ func TestAPIUpdateFile(t *testing.T) { assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) + gitRepo.Close() } // Test updating a file in a new branch diff --git a/integrations/api_repo_get_contents_list_test.go b/integrations/api_repo_get_contents_list_test.go index f74ceb514a..4605ccf4d9 100644 --- a/integrations/api_repo_get_contents_list_test.go +++ b/integrations/api_repo_get_contents_list_test.go @@ -74,6 +74,8 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { repo1.CreateNewBranch(user2, repo1.DefaultBranch, newBranch) // Get the commit ID of the default branch gitRepo, _ := git.OpenRepository(repo1.RepoPath()) + defer gitRepo.Close() + commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch) // Make a new tag in repo1 newTag := "test_tag" diff --git a/integrations/api_repo_get_contents_test.go b/integrations/api_repo_get_contents_test.go index f6a43bc5c6..77a827ec61 100644 --- a/integrations/api_repo_get_contents_test.go +++ b/integrations/api_repo_get_contents_test.go @@ -75,6 +75,8 @@ func testAPIGetContents(t *testing.T, u *url.URL) { repo1.CreateNewBranch(user2, repo1.DefaultBranch, newBranch) // Get the commit ID of the default branch gitRepo, _ := git.OpenRepository(repo1.RepoPath()) + defer gitRepo.Close() + commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch) // Make a new tag in repo1 newTag := "test_tag" diff --git a/integrations/api_repo_git_tags_test.go b/integrations/api_repo_git_tags_test.go index ae519249e0..d6ff08990a 100644 --- a/integrations/api_repo_git_tags_test.go +++ b/integrations/api_repo_git_tags_test.go @@ -29,6 +29,8 @@ func TestAPIGitTags(t *testing.T) { git.NewCommand("config", "user.email", user.Email).RunInDir(repo.RepoPath()) gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() + commit, _ := gitRepo.GetBranchCommit("master") lTagName := "lightweightTag" gitRepo.CreateTag(lTagName, commit.ID.String()) diff --git a/integrations/api_repo_test.go b/integrations/api_repo_test.go index a2683d4af4..1682f386d3 100644 --- a/integrations/api_repo_test.go +++ b/integrations/api_repo_test.go @@ -70,9 +70,9 @@ func TestAPISearchRepo(t *testing.T) { expectedResults }{ {name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 22}, - user: {count: 22}, - user2: {count: 22}}, + nil: {count: 24}, + user: {count: 24}, + user2: {count: 24}}, }, {name: "RepositoriesMax10", requestURL: "/api/v1/repos/search?limit=10&private=false", expectedResults: expectedResults{ nil: {count: 10}, @@ -92,7 +92,7 @@ func TestAPISearchRepo(t *testing.T) { {name: "RepositoriesAccessibleAndRelatedToUser", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user.ID), expectedResults: expectedResults{ nil: {count: 5}, user: {count: 9, includesPrivate: true}, - user2: {count: 5, includesPrivate: true}}, + user2: {count: 6, includesPrivate: true}}, }, {name: "RepositoriesAccessibleAndRelatedToUser2", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user2.ID), expectedResults: expectedResults{ nil: {count: 1}, @@ -103,7 +103,7 @@ func TestAPISearchRepo(t *testing.T) { {name: "RepositoriesAccessibleAndRelatedToUser3", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", user3.ID), expectedResults: expectedResults{ nil: {count: 1}, user: {count: 4, includesPrivate: true}, - user2: {count: 2, includesPrivate: true}, + user2: {count: 3, includesPrivate: true}, user3: {count: 4, includesPrivate: true}}, }, {name: "RepositoriesOwnedByOrganization", requestURL: fmt.Sprintf("/api/v1/repos/search?uid=%d", orgUser.ID), expectedResults: expectedResults{ diff --git a/integrations/api_team_test.go b/integrations/api_team_test.go index e25ffdf7b1..3a27cd4865 100644 --- a/integrations/api_team_test.go +++ b/integrations/api_team_test.go @@ -11,8 +11,8 @@ import ( "testing" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "github.com/stretchr/testify/assert" ) diff --git a/integrations/api_team_user_test.go b/integrations/api_team_user_test.go index 4df4dac016..f847c78949 100644 --- a/integrations/api_team_user_test.go +++ b/integrations/api_team_user_test.go @@ -10,8 +10,8 @@ import ( "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "github.com/stretchr/testify/assert" ) diff --git a/integrations/gitea-repositories-meta/user27/template1.git/HEAD b/integrations/gitea-repositories-meta/user27/template1.git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/integrations/gitea-repositories-meta/user27/template1.git/config b/integrations/gitea-repositories-meta/user27/template1.git/config new file mode 100644 index 0000000000..64280b806c --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = true + symlinks = false + ignorecase = true diff --git a/integrations/gitea-repositories-meta/user27/template1.git/description b/integrations/gitea-repositories-meta/user27/template1.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/applypatch-msg.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/applypatch-msg.sample new file mode 100644 index 0000000000..a5d7b84a67 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/commit-msg.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/commit-msg.sample new file mode 100644 index 0000000000..b58d1184a9 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/fsmonitor-watchman.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/fsmonitor-watchman.sample new file mode 100644 index 0000000000..e673bb3980 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/fsmonitor-watchman.sample @@ -0,0 +1,114 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 1) and a time in nanoseconds +# formatted as a string and outputs to stdout all files that have been +# modified since the given time. Paths must be relative to the root of +# the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $time) = @ARGV; + +# Check the hook interface version + +if ($version == 1) { + # convert nanoseconds to seconds + $time = int $time / 1000000000; +} else { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree; +if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $git_work_tree = Win32::GetCwd(); + $git_work_tree =~ tr/\\/\//; +} else { + require Cwd; + $git_work_tree = Cwd::cwd(); +} + +my $retry = 1; + +launch_watchman(); + +sub launch_watchman { + + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $time but were not transient (ie created after + # $time but no longer exist). + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + # + # The category of transient files that we want to ignore will have a + # creation clock (cclock) newer than $time_t value and will also not + # currently exist. + + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $time, + "fields": ["name"], + "expression": ["not", ["allof", ["since", $time, "cclock"], ["not", "exists"]]] + }] + END + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + my $json_pkg; + eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; + } or do { + require JSON::PP; + $json_pkg = "JSON::PP"; + }; + + my $o = $json_pkg->new->utf8->decode($response); + + if ($retry > 0 and $o->{error} and $o->{error} =~ m/unable to resolve root .* directory (.*) is not watched/) { + print STDERR "Adding '$git_work_tree' to watchman's watch list.\n"; + $retry--; + qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + print "/\0"; + eval { launch_watchman() }; + exit 0; + } + + die "Watchman: $o->{error}.\n" . + "Falling back to scanning...\n" if $o->{error}; + + binmode STDOUT, ":utf8"; + local $, = "\0"; + print @{$o->{files}}; +} diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive b/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive new file mode 100644 index 0000000000..1733c16a37 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +data=$(cat) +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)} + +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +test -x "${hook}" || continue +echo "${data}" | "${hook}" +exitcodes="${exitcodes} $?" +done + +for i in ${exitcodes}; do +[ ${i} -eq 0 ] || exit ${i} +done diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea b/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea new file mode 100644 index 0000000000..43a948da3a --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" post-receive diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-update.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-update.sample new file mode 100644 index 0000000000..ec17ec1939 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-applypatch.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-applypatch.sample new file mode 100644 index 0000000000..4142082bcb --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-commit.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-commit.sample new file mode 100644 index 0000000000..6a75641638 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-push.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-push.sample new file mode 100644 index 0000000000..6187dbf439 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +z40=0000000000000000000000000000000000000000 + +while read local_ref local_sha remote_ref remote_sha +do + if [ "$local_sha" = $z40 ] + then + # Handle delete + : + else + if [ "$remote_sha" = $z40 ] + then + # New branch, examine all commits + range="$local_sha" + else + # Update to existing branch, examine new commits + range="$remote_sha..$local_sha" + fi + + # Check for WIP commit + commit=`git rev-list -n 1 --grep '^WIP' "$range"` + if [ -n "$commit" ] + then + echo >&2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-rebase.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-rebase.sample new file mode 100644 index 0000000000..6cbef5c370 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive new file mode 100644 index 0000000000..1733c16a37 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +data=$(cat) +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)} + +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +test -x "${hook}" || continue +echo "${data}" | "${hook}" +exitcodes="${exitcodes} $?" +done + +for i in ${exitcodes}; do +[ ${i} -eq 0 ] || exit ${i} +done diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea new file mode 100644 index 0000000000..49d0940636 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" pre-receive diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.sample new file mode 100644 index 0000000000..a1fd29ec14 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/prepare-commit-msg.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/prepare-commit-msg.sample new file mode 100644 index 0000000000..10fa14c5ab --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/update b/integrations/gitea-repositories-meta/user27/template1.git/hooks/update new file mode 100644 index 0000000000..2918ffb7eb --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/update @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +exitcodes="" +hookname=$(basename $0) +GIT_DIR=${GIT_DIR:-$(dirname $0)} + +for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +test -x "${hook}" || continue +"${hook}" $1 $2 $3 +exitcodes="${exitcodes} $?" +done + +for i in ${exitcodes}; do +[ ${i} -eq 0 ] || exit ${i} +done diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea b/integrations/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea new file mode 100644 index 0000000000..38101c2426 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +"$GITEA_ROOT/gitea" hook --config="$GITEA_ROOT/$GITEA_CONF" update $1 $2 $3 diff --git a/integrations/gitea-repositories-meta/user27/template1.git/hooks/update.sample b/integrations/gitea-repositories-meta/user27/template1.git/hooks/update.sample new file mode 100644 index 0000000000..80ba94135c --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/hooks/update.sample @@ -0,0 +1,128 @@ +#!/bin/sh +# +# An example hook script to block unannotated tags from entering. +# Called by "git receive-pack" with arguments: refname sha1-old sha1-new +# +# To enable this hook, rename this file to "update". +# +# Config +# ------ +# hooks.allowunannotated +# This boolean sets whether unannotated tags will be allowed into the +# repository. By default they won't be. +# hooks.allowdeletetag +# This boolean sets whether deleting tags will be allowed in the +# repository. By default they won't be. +# hooks.allowmodifytag +# This boolean sets whether a tag may be modified after creation. By default +# it won't be. +# hooks.allowdeletebranch +# This boolean sets whether deleting branches will be allowed in the +# repository. By default they won't be. +# hooks.denycreatebranch +# This boolean sets whether remotely creating branches will be denied +# in the repository. By default this is allowed. +# + +# --- Command line +refname="$1" +oldrev="$2" +newrev="$3" + +# --- Safety check +if [ -z "$GIT_DIR" ]; then + echo "Don't run this script from the command line." >&2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --bool hooks.allowunannotated) +allowdeletebranch=$(git config --bool hooks.allowdeletebranch) +denycreatebranch=$(git config --bool hooks.denycreatebranch) +allowdeletetag=$(git config --bool hooks.allowdeletetag) +allowmodifytag=$(git config --bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero="0000000000000000000000000000000000000000" +if [ "$newrev" = "$zero" ]; then + newrev_type=delete +else + newrev_type=$(git cat-file -t $newrev) +fi + +case "$refname","$newrev_type" in + refs/tags/*,commit) + # un-annotated tag + short_refname=${refname##refs/tags/} + if [ "$allowunannotated" != "true" ]; then + echo "*** The un-annotated tag, $short_refname, is not allowed in this repository" >&2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/integrations/gitea-repositories-meta/user27/template1.git/info/exclude b/integrations/gitea-repositories-meta/user27/template1.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/info/refs b/integrations/gitea-repositories-meta/user27/template1.git/info/refs new file mode 100644 index 0000000000..22f08279c0 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/info/refs @@ -0,0 +1 @@ +aacbdfe9e1c4b47f60abe81849045fa4e96f1d75 refs/heads/master diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 b/integrations/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 new file mode 100644 index 0000000000..b6f121a4bb Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d b/integrations/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d new file mode 100644 index 0000000000..d2f4c1d04e Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061 b/integrations/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061 new file mode 100644 index 0000000000..aa34a8a767 Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061 differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52 b/integrations/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52 new file mode 100644 index 0000000000..3f9705fef4 Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52 differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 b/integrations/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 new file mode 100644 index 0000000000..74419f4b47 Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb b/integrations/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb new file mode 100644 index 0000000000..844eb1c2a6 Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f b/integrations/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f new file mode 100644 index 0000000000..0699bff833 Binary files /dev/null and b/integrations/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f differ diff --git a/integrations/gitea-repositories-meta/user27/template1.git/objects/info/packs b/integrations/gitea-repositories-meta/user27/template1.git/objects/info/packs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/objects/info/packs @@ -0,0 +1 @@ + diff --git a/integrations/gitea-repositories-meta/user27/template1.git/refs/heads/master b/integrations/gitea-repositories-meta/user27/template1.git/refs/heads/master new file mode 100644 index 0000000000..0f13243bfd --- /dev/null +++ b/integrations/gitea-repositories-meta/user27/template1.git/refs/heads/master @@ -0,0 +1 @@ +aacbdfe9e1c4b47f60abe81849045fa4e96f1d75 diff --git a/integrations/integration_test.go b/integrations/integration_test.go index 897315f2ae..8d7f47dcfb 100644 --- a/integrations/integration_test.go +++ b/integrations/integration_test.go @@ -18,6 +18,7 @@ import ( "os" "path" "path/filepath" + "runtime" "strings" "testing" @@ -102,7 +103,11 @@ func initIntegrationTest() { fmt.Println("Environment variable $GITEA_ROOT not set") os.Exit(1) } - setting.AppPath = path.Join(giteaRoot, "gitea") + giteaBinary := "gitea" + if runtime.GOOS == "windows" { + giteaBinary += ".exe" + } + setting.AppPath = path.Join(giteaRoot, giteaBinary) if _, err := os.Stat(setting.AppPath); err != nil { fmt.Printf("Could not find gitea binary at %s\n", setting.AppPath) os.Exit(1) diff --git a/integrations/pull_merge_test.go b/integrations/pull_merge_test.go index 27f9fc6bb9..a63e27e149 100644 --- a/integrations/pull_merge_test.go +++ b/integrations/pull_merge_test.go @@ -5,15 +5,22 @@ package integrations import ( + "bytes" + "fmt" "net/http" "net/http/httptest" "net/url" + "os" "path" "strings" "testing" + "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/services/pull" "github.com/stretchr/testify/assert" "github.com/unknwon/i18n" @@ -202,3 +209,133 @@ func TestCantMergeWorkInProgress(t *testing.T) { assert.Equal(t, replacer.Replace(expected), text, "Unable to find WIP text") }) } + +func TestCantMergeConflict(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + prepareTestEnv(t) + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") + + // Use API to create a conflicting pr + token := getTokenForLoggedInUser(t, session) + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{ + Head: "conflict", + Base: "base", + Title: "create a conflicting pr", + }) + session.MakeRequest(t, req, 201) + + // Now this PR will be marked conflict - or at least a race will do - so drop down to pure code at this point... + user1 := models.AssertExistsAndLoadBean(t, &models.User{ + Name: "user1", + }).(*models.User) + repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ + OwnerID: user1.ID, + Name: "repo1", + }).(*models.Repository) + + pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{ + HeadRepoID: repo1.ID, + BaseRepoID: repo1.ID, + HeadBranch: "conflict", + BaseBranch: "base", + }).(*models.PullRequest) + + gitRepo, err := git.OpenRepository(models.RepoPath(user1.Name, repo1.Name)) + assert.NoError(t, err) + + err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "CONFLICT") + assert.Error(t, err, "Merge should return an error due to conflict") + assert.True(t, models.IsErrMergeConflicts(err), "Merge error is not a conflict error") + + err = pull.Merge(pr, user1, gitRepo, models.MergeStyleRebase, "CONFLICT") + assert.Error(t, err, "Merge should return an error due to conflict") + assert.True(t, models.IsErrRebaseConflicts(err), "Merge error is not a conflict error") + }) +} + +func TestCantMergeUnrelated(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + prepareTestEnv(t) + session := loginUser(t, "user1") + testRepoFork(t, session, "user2", "repo1", "user1", "repo1") + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n") + + // Now we want to create a commit on a branch that is totally unrelated to our current head + // Drop down to pure code at this point + user1 := models.AssertExistsAndLoadBean(t, &models.User{ + Name: "user1", + }).(*models.User) + repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ + OwnerID: user1.ID, + Name: "repo1", + }).(*models.Repository) + path := models.RepoPath(user1.Name, repo1.Name) + + _, err := git.NewCommand("read-tree", "--empty").RunInDir(path) + assert.NoError(t, err) + + stdin := bytes.NewBufferString("Unrelated File") + var stdout strings.Builder + err = git.NewCommand("hash-object", "-w", "--stdin").RunInDirFullPipeline(path, &stdout, nil, stdin) + assert.NoError(t, err) + sha := strings.TrimSpace(stdout.String()) + + _, err = git.NewCommand("update-index", "--add", "--replace", "--cacheinfo", "100644", sha, "somewher-over-the-rainbow").RunInDir(path) + assert.NoError(t, err) + + treeSha, err := git.NewCommand("write-tree").RunInDir(path) + assert.NoError(t, err) + treeSha = strings.TrimSpace(treeSha) + + commitTimeStr := time.Now().Format(time.RFC3339) + doerSig := user1.NewGitSig() + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+doerSig.Name, + "GIT_AUTHOR_EMAIL="+doerSig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+doerSig.Name, + "GIT_COMMITTER_EMAIL="+doerSig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + messageBytes := new(bytes.Buffer) + _, _ = messageBytes.WriteString("Unrelated") + _, _ = messageBytes.WriteString("\n") + + stdout.Reset() + err = git.NewCommand("commit-tree", treeSha).RunInDirTimeoutEnvFullPipeline(env, -1, path, &stdout, nil, messageBytes) + assert.NoError(t, err) + commitSha := strings.TrimSpace(stdout.String()) + + _, err = git.NewCommand("branch", "unrelated", commitSha).RunInDir(path) + assert.NoError(t, err) + + testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n") + + // Use API to create a conflicting pr + token := getTokenForLoggedInUser(t, session) + req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{ + Head: "unrelated", + Base: "base", + Title: "create an unrelated pr", + }) + session.MakeRequest(t, req, 201) + + // Now this PR could be marked conflict - or at least a race may occur - so drop down to pure code at this point... + gitRepo, err := git.OpenRepository(path) + assert.NoError(t, err) + pr := models.AssertExistsAndLoadBean(t, &models.PullRequest{ + HeadRepoID: repo1.ID, + BaseRepoID: repo1.ID, + HeadBranch: "unrelated", + BaseBranch: "base", + }).(*models.PullRequest) + + err = pull.Merge(pr, user1, gitRepo, models.MergeStyleMerge, "UNRELATED") + assert.Error(t, err, "Merge should return an error due to unrelated") + assert.True(t, models.IsErrMergeUnrelatedHistories(err), "Merge error is not a unrelated histories error") + }) +} diff --git a/integrations/repo_generate_test.go b/integrations/repo_generate_test.go new file mode 100644 index 0000000000..b8f2a45e1e --- /dev/null +++ b/integrations/repo_generate_test.go @@ -0,0 +1,67 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "code.gitea.io/gitea/models" + + "github.com/stretchr/testify/assert" +) + +func testRepoGenerate(t *testing.T, session *TestSession, templateOwnerName, templateRepoName, generateOwnerName, generateRepoName string) *httptest.ResponseRecorder { + generateOwner := models.AssertExistsAndLoadBean(t, &models.User{Name: generateOwnerName}).(*models.User) + + // Step0: check the existence of the generated repo + req := NewRequestf(t, "GET", "/%s/%s", generateOwnerName, generateRepoName) + resp := session.MakeRequest(t, req, http.StatusNotFound) + + // Step1: go to the main page of template repo + req = NewRequestf(t, "GET", "/%s/%s", templateOwnerName, templateRepoName) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Step2: click the "Use this template" button + htmlDoc := NewHTMLParser(t, resp.Body) + link, exists := htmlDoc.doc.Find("a.ui.button[href^=\"/repo/create\"]").Attr("href") + assert.True(t, exists, "The template has changed") + req = NewRequest(t, "GET", link) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Step3: fill the form of the create + htmlDoc = NewHTMLParser(t, resp.Body) + link, exists = htmlDoc.doc.Find("form.ui.form[action^=\"/repo/create\"]").Attr("action") + assert.True(t, exists, "The template has changed") + _, exists = htmlDoc.doc.Find(fmt.Sprintf(".owner.dropdown .item[data-value=\"%d\"]", generateOwner.ID)).Attr("data-value") + assert.True(t, exists, fmt.Sprintf("Generate owner '%s' is not present in select box", generateOwnerName)) + req = NewRequestWithValues(t, "POST", link, map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "uid": fmt.Sprintf("%d", generateOwner.ID), + "repo_name": generateRepoName, + "git_content": "true", + }) + resp = session.MakeRequest(t, req, http.StatusFound) + + // Step4: check the existence of the generated repo + req = NewRequestf(t, "GET", "/%s/%s", generateOwnerName, generateRepoName) + resp = session.MakeRequest(t, req, http.StatusOK) + + return resp +} + +func TestRepoGenerate(t *testing.T) { + prepareTestEnv(t) + session := loginUser(t, "user1") + testRepoGenerate(t, session, "user27", "template1", "user1", "generated1") +} + +func TestRepoGenerateToOrg(t *testing.T) { + prepareTestEnv(t) + session := loginUser(t, "user2") + testRepoGenerate(t, session, "user27", "template1", "user2", "generated2") +} diff --git a/integrations/repo_watch_test.go b/integrations/repo_watch_test.go new file mode 100644 index 0000000000..d96b014a73 --- /dev/null +++ b/integrations/repo_watch_test.go @@ -0,0 +1,24 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package integrations + +import ( + "net/url" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/setting" +) + +func TestRepoWatch(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // Test round-trip auto-watch + setting.Service.AutoWatchOnChanges = true + session := loginUser(t, "user2") + models.AssertNotExistsBean(t, &models.Watch{UserID: 2, RepoID: 3}) + testEditFile(t, session, "user3", "repo3", "master", "README.md", "Hello, World (Edited for watch)\n") + models.AssertExistsAndLoadBean(t, &models.Watch{UserID: 2, RepoID: 3, Mode: models.RepoWatchModeAuto}) + }) +} diff --git a/integrations/repofiles_delete_test.go b/integrations/repofiles_delete_test.go index b4c535188b..754f64023f 100644 --- a/integrations/repofiles_delete_test.go +++ b/integrations/repofiles_delete_test.go @@ -73,6 +73,7 @@ func testDeleteRepoFile(t *testing.T, u *url.URL) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() repo := ctx.Repo.Repository doer := ctx.User opts := getDeleteRepoFileOptions(repo) @@ -111,6 +112,8 @@ func testDeleteRepoFileWithoutBranchNames(t *testing.T, u *url.URL) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User opts := getDeleteRepoFileOptions(repo) @@ -139,6 +142,8 @@ func TestDeleteRepoFileErrors(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User diff --git a/integrations/repofiles_update_test.go b/integrations/repofiles_update_test.go index c475c70008..35cb5e8b0c 100644 --- a/integrations/repofiles_update_test.go +++ b/integrations/repofiles_update_test.go @@ -191,6 +191,8 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User opts := getCreateRepoFileOptions(repo) @@ -201,6 +203,8 @@ func TestCreateOrUpdateRepoFileForCreate(t *testing.T) { // asserts assert.Nil(t, err) gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() + commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch) expectedFileResponse := getExpectedFileResponseForRepofilesCreate(commitID) assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) @@ -220,6 +224,8 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User opts := getUpdateRepoFileOptions(repo) @@ -230,6 +236,8 @@ func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) { // asserts assert.Nil(t, err) gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() + commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch) expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID, opts.TreePath) assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) @@ -249,6 +257,8 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User opts := getUpdateRepoFileOptions(repo) @@ -261,6 +271,8 @@ func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) { // asserts assert.Nil(t, err) gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() + commit, _ := gitRepo.GetBranchCommit(opts.NewBranch) expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commit.ID.String(), opts.TreePath) // assert that the old file no longer exists in the last commit of the branch @@ -288,6 +300,8 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User opts := getUpdateRepoFileOptions(repo) @@ -300,6 +314,8 @@ func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) { // asserts assert.Nil(t, err) gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() + commitID, _ := gitRepo.GetBranchCommitID(repo.DefaultBranch) expectedFileResponse := getExpectedFileResponseForRepofilesUpdate(commitID, opts.TreePath) assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) @@ -315,6 +331,8 @@ func TestCreateOrUpdateRepoFileErrors(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository doer := ctx.User diff --git a/models/consistency.go b/models/consistency.go index f9fa3028fd..62d1d2e874 100644 --- a/models/consistency.go +++ b/models/consistency.go @@ -84,14 +84,17 @@ func (user *User) checkForConsistency(t *testing.T) { func (repo *Repository) checkForConsistency(t *testing.T) { assert.Equal(t, repo.LowerName, strings.ToLower(repo.Name), "repo: %+v", repo) assertCount(t, &Star{RepoID: repo.ID}, repo.NumStars) - assertCount(t, &Watch{RepoID: repo.ID}, repo.NumWatches) assertCount(t, &Milestone{RepoID: repo.ID}, repo.NumMilestones) assertCount(t, &Repository{ForkID: repo.ID}, repo.NumForks) if repo.IsFork { AssertExistsAndLoadBean(t, &Repository{ID: repo.ForkID}) } - actual := getCount(t, x.Where("is_pull=?", false), &Issue{RepoID: repo.ID}) + actual := getCount(t, x.Where("Mode<>?", RepoWatchModeDont), &Watch{RepoID: repo.ID}) + assert.EqualValues(t, repo.NumWatches, actual, + "Unexpected number of watches for repo %+v", repo) + + actual = getCount(t, x.Where("is_pull=?", false), &Issue{RepoID: repo.ID}) assert.EqualValues(t, repo.NumIssues, actual, "Unexpected number of issues for repo %+v", repo) diff --git a/models/error.go b/models/error.go index 505df28868..313c36354d 100644 --- a/models/error.go +++ b/models/error.go @@ -1206,6 +1206,79 @@ func (err ErrInvalidMergeStyle) Error() string { err.ID, err.Style) } +// ErrMergeConflicts represents an error if merging fails with a conflict +type ErrMergeConflicts struct { + Style MergeStyle + StdOut string + StdErr string + Err error +} + +// IsErrMergeConflicts checks if an error is a ErrMergeConflicts. +func IsErrMergeConflicts(err error) bool { + _, ok := err.(ErrMergeConflicts) + return ok +} + +func (err ErrMergeConflicts) Error() string { + return fmt.Sprintf("Merge Conflict Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} + +// ErrMergeUnrelatedHistories represents an error if merging fails due to unrelated histories +type ErrMergeUnrelatedHistories struct { + Style MergeStyle + StdOut string + StdErr string + Err error +} + +// IsErrMergeUnrelatedHistories checks if an error is a ErrMergeUnrelatedHistories. +func IsErrMergeUnrelatedHistories(err error) bool { + _, ok := err.(ErrMergeUnrelatedHistories) + return ok +} + +func (err ErrMergeUnrelatedHistories) Error() string { + return fmt.Sprintf("Merge UnrelatedHistories Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} + +// ErrMergePushOutOfDate represents an error if merging fails due to unrelated histories +type ErrMergePushOutOfDate struct { + Style MergeStyle + StdOut string + StdErr string + Err error +} + +// IsErrMergePushOutOfDate checks if an error is a ErrMergePushOutOfDate. +func IsErrMergePushOutOfDate(err error) bool { + _, ok := err.(ErrMergePushOutOfDate) + return ok +} + +func (err ErrMergePushOutOfDate) Error() string { + return fmt.Sprintf("Merge PushOutOfDate Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) +} + +// ErrRebaseConflicts represents an error if rebase fails with a conflict +type ErrRebaseConflicts struct { + Style MergeStyle + CommitSHA string + StdOut string + StdErr string + Err error +} + +// IsErrRebaseConflicts checks if an error is a ErrRebaseConflicts. +func IsErrRebaseConflicts(err error) bool { + _, ok := err.(ErrRebaseConflicts) + return ok +} + +func (err ErrRebaseConflicts) Error() string { + return fmt.Sprintf("Rebase Error: %v: Whilst Rebasing: %s\n%s\n%s", err.Err, err.CommitSHA, err.StdErr, err.StdOut) +} + // _________ __ // \_ ___ \ ____ _____ _____ ____ _____/ |_ // / \ \/ / _ \ / \ / \_/ __ \ / \ __\ diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 014e5155ba..28c606da43 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -438,3 +438,17 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 + +- + id: 64 + repo_id: 44 + type: 1 + config: "{}" + created_unix: 946684810 + +- + id: 65 + repo_id: 45 + type: 1 + config: "{}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index cf7d24c6cd..4cec6471e9 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -10,7 +10,7 @@ num_closed_pulls: 0 num_milestones: 3 num_closed_milestones: 1 - num_watches: 3 + num_watches: 4 status: 0 - @@ -561,3 +561,29 @@ num_issues: 0 is_mirror: false status: 0 + +- + id: 44 + owner_id: 27 + lower_name: template1 + name: template1 + is_private: false + is_template: true + num_stars: 0 + num_forks: 0 + num_issues: 0 + is_mirror: false + status: 0 + +- + id: 45 + owner_id: 27 + lower_name: template2 + name: template2 + is_private: false + is_template: true + num_stars: 0 + num_forks: 0 + num_issues: 0 + is_mirror: false + status: 0 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index a204241f9c..5a3b04994c 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -427,4 +427,19 @@ num_repos: 1 num_members: 0 num_teams: 1 - repo_admin_change_team_access: true \ No newline at end of file + repo_admin_change_team_access: true + +- + id: 27 + lower_name: user27 + name: user27 + full_name: User Twenty-Seven + email: user27@example.com + email_notifications_preference: enabled + passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password + type: 0 # individual + salt: ZogKvWdyEx + is_admin: false + avatar: avatar27 + avatar_email: user27@example.com + num_repos: 2 diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml index 5cd3b55fc4..c29f6bb65a 100644 --- a/models/fixtures/watch.yml +++ b/models/fixtures/watch.yml @@ -2,13 +2,28 @@ id: 1 user_id: 1 repo_id: 1 + mode: 1 # normal - id: 2 user_id: 4 repo_id: 1 + mode: 1 # normal - id: 3 user_id: 9 repo_id: 1 + mode: 1 # normal + +- + id: 4 + user_id: 8 + repo_id: 1 + mode: 2 # don't watch + +- + id: 5 + user_id: 11 + repo_id: 1 + mode: 3 # auto diff --git a/models/graph_test.go b/models/graph_test.go index c1f0bc90d9..78bfcb27ec 100644 --- a/models/graph_test.go +++ b/models/graph_test.go @@ -17,6 +17,7 @@ func BenchmarkGetCommitGraph(b *testing.B) { if err != nil { b.Error("Could not open repository") } + defer currentRepo.Close() for i := 0; i < b.N; i++ { graph, err := GetCommitGraph(currentRepo, 1) diff --git a/models/issue_assignees.go b/models/issue_assignees.go index e15b718eb2..70bed039c2 100644 --- a/models/issue_assignees.go +++ b/models/issue_assignees.go @@ -7,6 +7,8 @@ package models import ( "fmt" + "code.gitea.io/gitea/modules/util" + "xorm.io/xorm" ) @@ -171,25 +173,20 @@ func toggleUserAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (remove // MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) { + var requestAssignees []string + // Keeping the old assigning method for compatibility reasons - if oneAssignee != "" { + if oneAssignee != "" && !util.IsStringInSlice(oneAssignee, multipleAssignees) { + requestAssignees = append(requestAssignees, oneAssignee) + } - // Prevent double adding assignees - var isDouble bool - for _, assignee := range multipleAssignees { - if assignee == oneAssignee { - isDouble = true - break - } - } - - if !isDouble { - multipleAssignees = append(multipleAssignees, oneAssignee) - } + //Prevent empty assignees + if len(multipleAssignees) > 0 && multipleAssignees[0] != "" { + requestAssignees = append(requestAssignees, multipleAssignees...) } // Get the IDs of all assignees - assigneeIDs, err = GetUserIDsByNames(multipleAssignees, false) + assigneeIDs, err = GetUserIDsByNames(requestAssignees, false) return } diff --git a/models/issue_assignees_test.go b/models/issue_assignees_test.go index 163234b167..79257013f8 100644 --- a/models/issue_assignees_test.go +++ b/models/issue_assignees_test.go @@ -59,3 +59,24 @@ func TestUpdateAssignee(t *testing.T) { assert.NoError(t, err) assert.False(t, isAssigned) } + +func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) { + IDs, err := MakeIDsFromAPIAssigneesToAdd("", []string{""}) + assert.NoError(t, err) + assert.Equal(t, []int64{}, IDs) + + IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"}) + assert.Error(t, err) + + IDs, err = MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"}) + assert.NoError(t, err) + assert.Equal(t, []int64{1}, IDs) + + IDs, err = MakeIDsFromAPIAssigneesToAdd("user2", []string{""}) + assert.NoError(t, err) + assert.Equal(t, []int64{2}, IDs) + + IDs, err = MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"}) + assert.NoError(t, err) + assert.Equal(t, []int64{1, 2}, IDs) +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 5ed70dc4f5..e5bfc2b881 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -266,6 +266,12 @@ var migrations = []Migration{ NewMigration("remove unnecessary columns from label", removeLabelUneededCols), // v105 -> v106 NewMigration("add includes_all_repositories to teams", addTeamIncludesAllRepositories), + // v106 -> v107 + NewMigration("add column `mode` to table watch", addModeColumnToWatch), + // v107 -> v108 + NewMigration("Add template options to repository", addTemplateToRepo), + // v108 -> v109 + NewMigration("Add comment_id on table notification", addCommentIDOnNotification), } // Migrate database to current version diff --git a/models/migrations/v106.go b/models/migrations/v106.go new file mode 100644 index 0000000000..201fc10266 --- /dev/null +++ b/models/migrations/v106.go @@ -0,0 +1,26 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +// RepoWatchMode specifies what kind of watch the user has on a repository +type RepoWatchMode int8 + +// Watch is connection request for receiving repository notification. +type Watch struct { + ID int64 `xorm:"pk autoincr"` + Mode RepoWatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"` +} + +func addModeColumnToWatch(x *xorm.Engine) (err error) { + if err = x.Sync2(new(Watch)); err != nil { + return + } + _, err = x.Exec("UPDATE `watch` SET `mode` = 1") + return err +} diff --git a/models/migrations/v107.go b/models/migrations/v107.go new file mode 100644 index 0000000000..3d6aeebaf0 --- /dev/null +++ b/models/migrations/v107.go @@ -0,0 +1,19 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addTemplateToRepo(x *xorm.Engine) error { + + type Repository struct { + IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"` + TemplateID int64 `xorm:"INDEX"` + } + + return x.Sync2(new(Repository)) +} diff --git a/models/migrations/v108.go b/models/migrations/v108.go new file mode 100644 index 0000000000..60b8fb47ae --- /dev/null +++ b/models/migrations/v108.go @@ -0,0 +1,18 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "xorm.io/xorm" +) + +func addCommentIDOnNotification(x *xorm.Engine) error { + type Notification struct { + ID int64 `xorm:"pk autoincr"` + CommentID int64 + } + + return x.Sync2(new(Notification)) +} diff --git a/models/migrations/v28.go b/models/migrations/v28.go index 587e944ce6..a849fea3c2 100644 --- a/models/migrations/v28.go +++ b/models/migrations/v28.go @@ -60,9 +60,9 @@ func addRepoSize(x *xorm.Engine) (err error) { } repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(user.Name), strings.ToLower(repo.Name)) + ".git" - countObject, err := git.GetRepoSize(repoPath) + countObject, err := git.CountObjects(repoPath) if err != nil { - log.Warn("GetRepoSize: %v", err) + log.Warn("CountObjects: %v", err) continue } diff --git a/models/migrations/v39.go b/models/migrations/v39.go index f3b32ea873..dc5f6ee091 100644 --- a/models/migrations/v39.go +++ b/models/migrations/v39.go @@ -47,6 +47,7 @@ func releaseAddColumnIsTagAndSyncTags(x *xorm.Engine) error { if err = models.SyncReleasesWithTags(repo, gitRepo); err != nil { log.Warn("SyncReleasesWithTags: %v", err) } + gitRepo.Close() } if len(repos) < pageSize { break diff --git a/models/migrations/v82.go b/models/migrations/v82.go index 3fb4b6c59e..2daa86ab07 100644 --- a/models/migrations/v82.go +++ b/models/migrations/v82.go @@ -91,6 +91,7 @@ func fixReleaseSha1OnReleaseTable(x *xorm.Engine) error { if err != nil { return err } + defer gitRepo.Close() gitRepoCache[release.RepoID] = gitRepo } diff --git a/models/notification.go b/models/notification.go index 5b6ce597d1..c8203754f9 100644 --- a/models/notification.go +++ b/models/notification.go @@ -44,8 +44,10 @@ type Notification struct { Status NotificationStatus `xorm:"SMALLINT INDEX NOT NULL"` Source NotificationSource `xorm:"SMALLINT INDEX NOT NULL"` - IssueID int64 `xorm:"INDEX NOT NULL"` - CommitID string `xorm:"INDEX"` + IssueID int64 `xorm:"INDEX NOT NULL"` + CommitID string `xorm:"INDEX"` + CommentID int64 + Comment *Comment `xorm:"-"` UpdatedBy int64 `xorm:"INDEX NOT NULL"` @@ -58,22 +60,27 @@ type Notification struct { // CreateOrUpdateIssueNotifications creates an issue notification // for each watcher, or updates it if already exists -func CreateOrUpdateIssueNotifications(issue *Issue, notificationAuthorID int64) error { +func CreateOrUpdateIssueNotifications(issueID, commentID int64, notificationAuthorID int64) error { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { return err } - if err := createOrUpdateIssueNotifications(sess, issue, notificationAuthorID); err != nil { + if err := createOrUpdateIssueNotifications(sess, issueID, commentID, notificationAuthorID); err != nil { return err } return sess.Commit() } -func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthorID int64) error { - issueWatches, err := getIssueWatchers(e, issue.ID) +func createOrUpdateIssueNotifications(e Engine, issueID, commentID int64, notificationAuthorID int64) error { + issueWatches, err := getIssueWatchers(e, issueID) + if err != nil { + return err + } + + issue, err := getIssueByID(e, issueID) if err != nil { return err } @@ -83,7 +90,7 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor return err } - notifications, err := getNotificationsByIssueID(e, issue.ID) + notifications, err := getNotificationsByIssueID(e, issueID) if err != nil { return err } @@ -102,9 +109,9 @@ func createOrUpdateIssueNotifications(e Engine, issue *Issue, notificationAuthor alreadyNotified[userID] = struct{}{} if notificationExists(notifications, issue.ID, userID) { - return updateIssueNotification(e, userID, issue.ID, notificationAuthorID) + return updateIssueNotification(e, userID, issue.ID, commentID, notificationAuthorID) } - return createIssueNotification(e, userID, issue, notificationAuthorID) + return createIssueNotification(e, userID, issue, commentID, notificationAuthorID) } for _, issueWatch := range issueWatches { @@ -157,12 +164,13 @@ func notificationExists(notifications []*Notification, issueID, userID int64) bo return false } -func createIssueNotification(e Engine, userID int64, issue *Issue, updatedByID int64) error { +func createIssueNotification(e Engine, userID int64, issue *Issue, commentID, updatedByID int64) error { notification := &Notification{ UserID: userID, RepoID: issue.RepoID, Status: NotificationStatusUnread, IssueID: issue.ID, + CommentID: commentID, UpdatedBy: updatedByID, } @@ -176,16 +184,25 @@ func createIssueNotification(e Engine, userID int64, issue *Issue, updatedByID i return err } -func updateIssueNotification(e Engine, userID, issueID, updatedByID int64) error { +func updateIssueNotification(e Engine, userID, issueID, commentID, updatedByID int64) error { notification, err := getIssueNotification(e, userID, issueID) if err != nil { return err } - notification.Status = NotificationStatusUnread - notification.UpdatedBy = updatedByID + // NOTICE: Only update comment id when the before notification on this issue is read, otherwise you may miss some old comments. + // But we need update update_by so that the notification will be reorder + var cols []string + if notification.Status == NotificationStatusRead { + notification.Status = NotificationStatusUnread + notification.CommentID = commentID + cols = []string{"status", "update_by", "comment_id"} + } else { + notification.UpdatedBy = updatedByID + cols = []string{"update_by"} + } - _, err = e.ID(notification.ID).Update(notification) + _, err = e.ID(notification.ID).Cols(cols...).Update(notification) return err } @@ -199,7 +216,7 @@ func getIssueNotification(e Engine, userID, issueID int64) (*Notification, error } // NotificationsForUser returns notifications for a given user and status -func NotificationsForUser(user *User, statuses []NotificationStatus, page, perPage int) ([]*Notification, error) { +func NotificationsForUser(user *User, statuses []NotificationStatus, page, perPage int) (NotificationList, error) { return notificationsForUser(x, user, statuses, page, perPage) } @@ -239,6 +256,204 @@ func (n *Notification) GetIssue() (*Issue, error) { return n.Issue, err } +// HTMLURL formats a URL-string to the notification +func (n *Notification) HTMLURL() string { + if n.Comment != nil { + return n.Comment.HTMLURL() + } + return n.Issue.HTMLURL() +} + +// NotificationList contains a list of notifications +type NotificationList []*Notification + +func (nl NotificationList) getPendingRepoIDs() []int64 { + var ids = make(map[int64]struct{}, len(nl)) + for _, notification := range nl { + if notification.Repository != nil { + continue + } + if _, ok := ids[notification.RepoID]; !ok { + ids[notification.RepoID] = struct{}{} + } + } + return keysInt64(ids) +} + +// LoadRepos loads repositories from database +func (nl NotificationList) LoadRepos() (RepositoryList, error) { + if len(nl) == 0 { + return RepositoryList{}, nil + } + + var repoIDs = nl.getPendingRepoIDs() + var repos = make(map[int64]*Repository, len(repoIDs)) + var left = len(repoIDs) + for left > 0 { + var limit = defaultMaxInSize + if left < limit { + limit = left + } + rows, err := x. + In("id", repoIDs[:limit]). + Rows(new(Repository)) + if err != nil { + return nil, err + } + + for rows.Next() { + var repo Repository + err = rows.Scan(&repo) + if err != nil { + rows.Close() + return nil, err + } + + repos[repo.ID] = &repo + } + _ = rows.Close() + + left -= limit + repoIDs = repoIDs[limit:] + } + + var reposList = make(RepositoryList, 0, len(repoIDs)) + for _, notification := range nl { + if notification.Repository == nil { + notification.Repository = repos[notification.RepoID] + } + var found bool + for _, r := range reposList { + if r.ID == notification.Repository.ID { + found = true + break + } + } + if !found { + reposList = append(reposList, notification.Repository) + } + } + return reposList, nil +} + +func (nl NotificationList) getPendingIssueIDs() []int64 { + var ids = make(map[int64]struct{}, len(nl)) + for _, notification := range nl { + if notification.Issue != nil { + continue + } + if _, ok := ids[notification.IssueID]; !ok { + ids[notification.IssueID] = struct{}{} + } + } + return keysInt64(ids) +} + +// LoadIssues loads issues from database +func (nl NotificationList) LoadIssues() error { + if len(nl) == 0 { + return nil + } + + var issueIDs = nl.getPendingIssueIDs() + var issues = make(map[int64]*Issue, len(issueIDs)) + var left = len(issueIDs) + for left > 0 { + var limit = defaultMaxInSize + if left < limit { + limit = left + } + rows, err := x. + In("id", issueIDs[:limit]). + Rows(new(Issue)) + if err != nil { + return err + } + + for rows.Next() { + var issue Issue + err = rows.Scan(&issue) + if err != nil { + rows.Close() + return err + } + + issues[issue.ID] = &issue + } + _ = rows.Close() + + left -= limit + issueIDs = issueIDs[limit:] + } + + for _, notification := range nl { + if notification.Issue == nil { + notification.Issue = issues[notification.IssueID] + notification.Issue.Repo = notification.Repository + } + } + return nil +} + +func (nl NotificationList) getPendingCommentIDs() []int64 { + var ids = make(map[int64]struct{}, len(nl)) + for _, notification := range nl { + if notification.CommentID == 0 || notification.Comment != nil { + continue + } + if _, ok := ids[notification.CommentID]; !ok { + ids[notification.CommentID] = struct{}{} + } + } + return keysInt64(ids) +} + +// LoadComments loads comments from database +func (nl NotificationList) LoadComments() error { + if len(nl) == 0 { + return nil + } + + var commentIDs = nl.getPendingCommentIDs() + var comments = make(map[int64]*Comment, len(commentIDs)) + var left = len(commentIDs) + for left > 0 { + var limit = defaultMaxInSize + if left < limit { + limit = left + } + rows, err := x. + In("id", commentIDs[:limit]). + Rows(new(Comment)) + if err != nil { + return err + } + + for rows.Next() { + var comment Comment + err = rows.Scan(&comment) + if err != nil { + rows.Close() + return err + } + + comments[comment.ID] = &comment + } + _ = rows.Close() + + left -= limit + commentIDs = commentIDs[limit:] + } + + for _, notification := range nl { + if notification.CommentID > 0 && notification.Comment == nil { + notification.Comment = comments[notification.CommentID] + notification.Comment.Issue = notification.Issue + } + } + return nil +} + // GetNotificationCount returns the notification count for user func GetNotificationCount(user *User, status NotificationStatus) (int64, error) { return getNotificationCount(x, user, status) diff --git a/models/notification_test.go b/models/notification_test.go index 01c960c929..728be7182c 100644 --- a/models/notification_test.go +++ b/models/notification_test.go @@ -14,7 +14,7 @@ func TestCreateOrUpdateIssueNotifications(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue) - assert.NoError(t, CreateOrUpdateIssueNotifications(issue, 2)) + assert.NoError(t, CreateOrUpdateIssueNotifications(issue.ID, 0, 2)) // User 9 is inactive, thus notifications for user 1 and 4 are created notf := AssertExistsAndLoadBean(t, &Notification{UserID: 1, IssueID: issue.ID}).(*Notification) diff --git a/models/pull.go b/models/pull.go index 45a1daac46..b25c52571d 100644 --- a/models/pull.go +++ b/models/pull.go @@ -380,6 +380,7 @@ func (pr *PullRequest) GetLastCommitStatus() (status *CommitStatus, err error) { if err != nil { return nil, err } + defer headGitRepo.Close() lastCommitID, err := headGitRepo.GetBranchCommitID(pr.HeadBranch) if err != nil { @@ -569,6 +570,7 @@ func (pr *PullRequest) getMergeCommit() (*git.Commit, error) { if err != nil { return nil, fmt.Errorf("OpenRepository: %v", err) } + defer gitRepo.Close() commit, err := gitRepo.GetCommit(mergeCommit[:40]) if err != nil { @@ -870,6 +872,7 @@ func (pr *PullRequest) UpdatePatch() (err error) { if err != nil { return fmt.Errorf("OpenRepository: %v", err) } + defer headGitRepo.Close() // Add a temporary remote. tmpRemote := com.ToStr(time.Now().UnixNano()) @@ -911,6 +914,7 @@ func (pr *PullRequest) PushToBaseRepo() (err error) { if err != nil { return fmt.Errorf("OpenRepository: %v", err) } + defer headGitRepo.Close() tmpRemoteName := fmt.Sprintf("tmp-pull-%d", pr.ID) if err = headGitRepo.AddRemote(tmpRemoteName, pr.BaseRepo.RepoPath(), false); err != nil { diff --git a/models/repo.go b/models/repo.go index 812460e92f..a340a391a1 100644 --- a/models/repo.go +++ b/models/repo.go @@ -36,6 +36,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "github.com/mcuadros/go-version" "github.com/unknwon/com" @@ -178,6 +179,9 @@ type Repository struct { IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"` ForkID int64 `xorm:"INDEX"` BaseRepo *Repository `xorm:"-"` + IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"` + TemplateID int64 `xorm:"INDEX"` + TemplateRepo *Repository `xorm:"-"` Size int64 `xorm:"NOT NULL DEFAULT 0"` IndexerStatus *RepoIndexerStatus `xorm:"-"` IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"` @@ -350,6 +354,7 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool) FullName: repo.FullName(), Description: repo.Description, Private: repo.IsPrivate, + Template: repo.IsTemplate, Empty: repo.IsEmpty, Archived: repo.IsArchived, Size: int(repo.Size / 1024), @@ -662,6 +667,27 @@ func (repo *Repository) getBaseRepo(e Engine) (err error) { return err } +// IsGenerated returns whether _this_ repository was generated from a template +func (repo *Repository) IsGenerated() bool { + return repo.TemplateID != 0 +} + +// GetTemplateRepo populates repo.TemplateRepo for a generated repository and +// returns an error on failure (NOTE: no error is returned for +// non-generated repositories, and TemplateRepo will be left untouched) +func (repo *Repository) GetTemplateRepo() (err error) { + return repo.getTemplateRepo(x) +} + +func (repo *Repository) getTemplateRepo(e Engine) (err error) { + if !repo.IsGenerated() { + return nil + } + + repo.TemplateRepo, err = getRepositoryByID(e, repo.TemplateID) + return err +} + func (repo *Repository) repoPath(e Engine) string { return RepoPath(repo.mustOwnerName(e), repo.Name) } @@ -708,17 +734,17 @@ func (repo *Repository) IsOwnedBy(userID int64) bool { } func (repo *Repository) updateSize(e Engine) error { - repoInfoSize, err := git.GetRepoSize(repo.repoPath(e)) + size, err := util.GetDirectorySize(repo.repoPath(e)) if err != nil { return fmt.Errorf("UpdateSize: %v", err) } - repo.Size = repoInfoSize.Size + repoInfoSize.SizePack + repo.Size = size _, err = e.ID(repo.ID).Cols("size").Update(repo) return err } -// UpdateSize updates the repository size, calculating it using git.GetRepoSize +// UpdateSize updates the repository size, calculating it using util.GetDirectorySize func (repo *Repository) UpdateSize() error { return repo.updateSize(x) } @@ -1021,6 +1047,7 @@ func MigrateRepositoryGitData(doer, u *User, repo *Repository, opts api.MigrateR if err != nil { return repo, fmt.Errorf("OpenRepository: %v", err) } + defer gitRepo.Close() repo.IsEmpty, err = gitRepo.IsEmpty() if err != nil { @@ -1219,6 +1246,20 @@ type CreateRepoOptions struct { Status RepositoryStatus } +// GenerateRepoOptions contains the template units to generate +type GenerateRepoOptions struct { + Name string + Description string + Private bool + GitContent bool + Topics bool +} + +// IsValid checks whether at least one option is chosen for generation +func (gro GenerateRepoOptions) IsValid() bool { + return gro.GitContent || gro.Topics // or other items as they are added +} + func getRepoInitFile(tp, name string) ([]byte, error) { cleanedName := strings.TrimLeft(path.Clean("/"+name), "/") relPath := path.Join("options", tp, cleanedName) @@ -1322,8 +1363,55 @@ func prepareRepoCommit(e Engine, repo *Repository, tmpDir, repoPath string, opts return nil } -// InitRepository initializes README and .gitignore if needed. -func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts CreateRepoOptions) (err error) { +func generateRepoCommit(e Engine, repo, templateRepo *Repository, tmpDir string) error { + commitTimeStr := time.Now().Format(time.RFC3339) + authorSig := repo.Owner.NewGitSig() + + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+authorSig.Name, + "GIT_AUTHOR_EMAIL="+authorSig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+authorSig.Name, + "GIT_COMMITTER_EMAIL="+authorSig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + // Clone to temporary path and do the init commit. + templateRepoPath := templateRepo.repoPath(e) + _, stderr, err := process.GetManager().ExecDirEnv( + -1, "", + fmt.Sprintf("generateRepoCommit(git clone): %s", templateRepoPath), + env, + git.GitExecutable, "clone", "--depth", "1", templateRepoPath, tmpDir, + ) + if err != nil { + return fmt.Errorf("git clone: %v - %s", err, stderr) + } + + if err := os.RemoveAll(path.Join(tmpDir, ".git")); err != nil { + return fmt.Errorf("remove git dir: %v", err) + } + + if err := git.InitRepository(tmpDir, false); err != nil { + return err + } + + repoPath := repo.repoPath(e) + _, stderr, err = process.GetManager().ExecDirEnv( + -1, tmpDir, + fmt.Sprintf("generateRepoCommit(git remote add): %s", repoPath), + env, + git.GitExecutable, "remote", "add", "origin", repoPath, + ) + if err != nil { + return fmt.Errorf("git remote add: %v - %s", err, stderr) + } + + return initRepoCommit(tmpDir, repo.Owner) +} + +func checkInitRepository(repoPath string) (err error) { // Somehow the directory could exist. if com.IsExist(repoPath) { return fmt.Errorf("initRepository: path already exists: %s", repoPath) @@ -1335,6 +1423,14 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C } else if err = createDelegateHooks(repoPath); err != nil { return fmt.Errorf("createDelegateHooks: %v", err) } + return nil +} + +// InitRepository initializes README and .gitignore if needed. +func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts CreateRepoOptions) (err error) { + if err = checkInitRepository(repoPath); err != nil { + return err + } tmpDir := filepath.Join(os.TempDir(), "gitea-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond())) @@ -1375,6 +1471,37 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C return nil } +// generateRepository initializes repository from template +func generateRepository(e Engine, repo, templateRepo *Repository) (err error) { + tmpDir := filepath.Join(os.TempDir(), "gitea-"+repo.Name+"-"+com.ToStr(time.Now().Nanosecond())) + + if err := os.MkdirAll(tmpDir, os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %v", tmpDir, err) + } + + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + log.Error("RemoveAll: %v", err) + } + }() + + if err = generateRepoCommit(e, repo, templateRepo, tmpDir); err != nil { + return fmt.Errorf("generateRepoCommit: %v", err) + } + + // re-fetch repo + if repo, err = getRepositoryByID(e, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %v", err) + } + + repo.DefaultBranch = "master" + if err = updateRepository(e, repo, false); err != nil { + return fmt.Errorf("updateRepository: %v", err) + } + + return nil +} + var ( reservedRepoNames = []string{".", ".."} reservedRepoPatterns = []string{"*.git", "*.wiki"} @@ -1469,16 +1596,6 @@ func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err err return fmt.Errorf("watchRepo: %v", err) } } - if err = notifyWatchers(e, &Action{ - ActUserID: doer.ID, - ActUser: doer, - OpType: ActionCreateRepo, - RepoID: repo.ID, - Repo: repo, - IsPrivate: repo.IsPrivate, - }); err != nil { - return fmt.Errorf("notify watchers '%d/%d': %v", doer.ID, repo.ID, err) - } if err = copyDefaultWebhooksToRepo(e, repo.ID); err != nil { return fmt.Errorf("copyDefaultWebhooksToRepo: %v", err) @@ -2410,8 +2527,8 @@ func CheckRepoStats() { checkers := []*repoChecker{ // Repository.NumWatches { - "SELECT repo.id FROM `repository` repo WHERE repo.num_watches!=(SELECT COUNT(*) FROM `watch` WHERE repo_id=repo.id)", - "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=?) WHERE id=?", + "SELECT repo.id FROM `repository` repo WHERE repo.num_watches!=(SELECT COUNT(*) FROM `watch` WHERE repo_id=repo.id AND mode<>2)", + "UPDATE `repository` SET num_watches=(SELECT COUNT(*) FROM `watch` WHERE repo_id=? AND mode<>2) WHERE id=?", "repository count 'num_watches'", }, // Repository.NumStars @@ -2532,6 +2649,28 @@ func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) { return repo, has } +// CopyLFS copies LFS data from one repo to another +func CopyLFS(newRepo, oldRepo *Repository) error { + return copyLFS(x, newRepo, oldRepo) +} + +func copyLFS(e Engine, newRepo, oldRepo *Repository) error { + var lfsObjects []*LFSMetaObject + if err := e.Where("repository_id=?", oldRepo.ID).Find(&lfsObjects); err != nil { + return err + } + + for _, v := range lfsObjects { + v.ID = 0 + v.RepositoryID = newRepo.ID + if _, err := e.Insert(v); err != nil { + return err + } + } + + return nil +} + // ForkRepository forks a repository func ForkRepository(doer, owner *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) { forkedRepo, err := oldRepo.GetUserFork(owner.ID) @@ -2602,27 +2741,73 @@ func ForkRepository(doer, owner *User, oldRepo *Repository, name, desc string) ( log.Error("Failed to update size for repository: %v", err) } - // Copy LFS meta objects in new session - sess2 := x.NewSession() - defer sess2.Close() - if err = sess2.Begin(); err != nil { + return repo, CopyLFS(repo, oldRepo) +} + +// GenerateRepository generates a repository from a template +func GenerateRepository(doer, owner *User, templateRepo *Repository, opts GenerateRepoOptions) (_ *Repository, err error) { + repo := &Repository{ + OwnerID: owner.ID, + Owner: owner, + Name: opts.Name, + LowerName: strings.ToLower(opts.Name), + Description: opts.Description, + IsPrivate: opts.Private, + IsEmpty: !opts.GitContent || templateRepo.IsEmpty, + IsFsckEnabled: templateRepo.IsFsckEnabled, + TemplateID: templateRepo.ID, + } + + createSess := x.NewSession() + defer createSess.Close() + if err = createSess.Begin(); err != nil { + return nil, err + } + + if err = createRepository(createSess, doer, owner, repo); err != nil { + return nil, err + } + + //Commit repo to get created repo ID + err = createSess.Commit() + if err != nil { + return nil, err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { return repo, err } - var lfsObjects []*LFSMetaObject - if err = sess2.Where("repository_id=?", oldRepo.ID).Find(&lfsObjects); err != nil { + repoPath := RepoPath(owner.Name, repo.Name) + if err = checkInitRepository(repoPath); err != nil { return repo, err } - for _, v := range lfsObjects { - v.ID = 0 - v.RepositoryID = repo.ID - if _, err = sess2.Insert(v); err != nil { + if opts.GitContent && !templateRepo.IsEmpty { + if err = generateRepository(sess, repo, templateRepo); err != nil { return repo, err } + + if err = repo.updateSize(sess); err != nil { + return repo, fmt.Errorf("failed to update size for repository: %v", err) + } + + if err = copyLFS(sess, repo, templateRepo); err != nil { + return repo, fmt.Errorf("failed to copy LFS: %v", err) + } } - return repo, sess2.Commit() + if opts.Topics { + for _, topic := range templateRepo.Topics { + if _, err = addTopicByNameToRepo(sess, repo.ID, topic); err != nil { + return repo, err + } + } + } + + return repo, sess.Commit() } // GetForks returns all the forks of the repository diff --git a/models/repo_activity.go b/models/repo_activity.go index aa5c2217e0..d25524c057 100644 --- a/models/repo_activity.go +++ b/models/repo_activity.go @@ -64,6 +64,8 @@ func GetActivityStats(repo *Repository, timeFrom time.Time, releases, issues, pr if err != nil { return nil, fmt.Errorf("OpenRepository: %v", err) } + defer gitRepo.Close() + code, err := gitRepo.GetCodeActivityStats(timeFrom, repo.DefaultBranch) if err != nil { return nil, fmt.Errorf("FillFromGit: %v", err) @@ -79,6 +81,8 @@ func GetActivityStatsTopAuthors(repo *Repository, timeFrom time.Time, count int) if err != nil { return nil, fmt.Errorf("OpenRepository: %v", err) } + defer gitRepo.Close() + code, err := gitRepo.GetCodeActivityStats(timeFrom, "") if err != nil { return nil, fmt.Errorf("FillFromGit: %v", err) diff --git a/models/repo_branch.go b/models/repo_branch.go index dee6ef3d7e..c513231836 100644 --- a/models/repo_branch.go +++ b/models/repo_branch.go @@ -23,6 +23,7 @@ func (repo *Repository) GetBranch(branch string) (*git.Branch, error) { if err != nil { return nil, err } + defer gitRepo.Close() return gitRepo.GetBranch(branch) } @@ -38,6 +39,7 @@ func (repo *Repository) CheckBranchName(name string) error { if err != nil { return err } + defer gitRepo.Close() branches, err := repo.GetBranches() if err != nil { @@ -94,6 +96,7 @@ func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName st log.Error("Unable to open temporary repository: %s (%v)", basePath, err) return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err) } + defer gitRepo.Close() if err = gitRepo.CreateBranch(branchName, oldBranchName); err != nil { log.Error("Unable to create branch: %s from %s. (%v)", branchName, oldBranchName, err) @@ -140,6 +143,7 @@ func (repo *Repository) CreateNewBranchFromCommit(doer *User, commit, branchName log.Error("Unable to open temporary repository: %s (%v)", basePath, err) return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err) } + defer gitRepo.Close() if err = gitRepo.CreateBranch(branchName, commit); err != nil { log.Error("Unable to create branch: %s from %s. (%v)", branchName, commit, err) diff --git a/models/repo_list.go b/models/repo_list.go index c823647eba..34fac8b055 100644 --- a/models/repo_list.go +++ b/models/repo_list.go @@ -111,17 +111,18 @@ func (repos MirrorRepositoryList) LoadAttributes() error { // SearchRepoOptions holds the search options type SearchRepoOptions struct { - UserID int64 - UserIsAdmin bool - Keyword string - OwnerID int64 - OrderBy SearchOrderBy - Private bool // Include private repositories in results - StarredByID int64 - Page int - IsProfile bool - AllPublic bool // Include also all public repositories - PageSize int // Can be smaller than or equal to setting.ExplorePagingNum + UserID int64 + UserIsAdmin bool + Keyword string + OwnerID int64 + PriorityOwnerID int64 + OrderBy SearchOrderBy + Private bool // Include private repositories in results + StarredByID int64 + Page int + IsProfile bool + AllPublic bool // Include also all public repositories + PageSize int // Can be smaller than or equal to setting.ExplorePagingNum // None -> include collaborative AND non-collaborative // True -> include just collaborative // False -> incude just non-collaborative @@ -130,6 +131,10 @@ type SearchRepoOptions struct { // True -> include just forks // False -> include just non-forks Fork util.OptionalBool + // None -> include templates AND non-templates + // True -> include just templates + // False -> include just non-templates + Template util.OptionalBool // None -> include mirrors AND non-mirrors // True -> include just mirrors // False -> include just non-mirrors @@ -190,6 +195,10 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { cond = cond.And(accessCond) } + if opts.Template != util.OptionalBoolNone { + cond = cond.And(builder.Eq{"is_template": opts.Template == util.OptionalBoolTrue}) + } + // Restrict to starred repositories if opts.StarredByID > 0 { cond = cond.And(builder.In("id", builder.Select("repo_id").From("star").Where(builder.Eq{"uid": opts.StarredByID}))) @@ -266,6 +275,10 @@ func SearchRepository(opts *SearchRepoOptions) (RepositoryList, int64, error) { opts.OrderBy = SearchOrderByAlphabetically } + if opts.PriorityOwnerID > 0 { + opts.OrderBy = SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = %d THEN 0 ELSE owner_id END, %s", opts.PriorityOwnerID, opts.OrderBy)) + } + sess := x.NewSession() defer sess.Close() @@ -308,11 +321,15 @@ func accessibleRepositoryCondition(userID int64) builder.Cond { builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePrivate}))), ), // 2. Be able to see all repositories that we have access to - builder.In("`repository`.id", builder.Select("repo_id"). - From("`access`"). - Where(builder.And( - builder.Eq{"user_id": userID}, - builder.Gt{"mode": int(AccessModeNone)}))), + builder.Or( + builder.In("`repository`.id", builder.Select("repo_id"). + From("`access`"). + Where(builder.And( + builder.Eq{"user_id": userID}, + builder.Gt{"mode": int(AccessModeNone)}))), + builder.In("`repository`.id", builder.Select("id"). + From("`repository`"). + Where(builder.Eq{"owner_id": userID}))), // 3. Be able to see all repositories that we are in a team builder.In("`repository`.id", builder.Select("`team_repo`.repo_id"). From("team_repo"). diff --git a/models/repo_list_test.go b/models/repo_list_test.go index e3a7acd4a4..b1dbf46af0 100644 --- a/models/repo_list_test.go +++ b/models/repo_list_test.go @@ -174,10 +174,10 @@ func TestSearchRepository(t *testing.T) { opts: &SearchRepoOptions{Keyword: "big_test_", Page: 1, PageSize: 10, Private: true, AllPublic: true, Collaborate: util.OptionalBoolFalse}, count: 14}, {name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse}, count: 22}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true, Template: util.OptionalBoolFalse}, count: 28}, {name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 15, Private: true, AllPublic: true}, @@ -186,8 +186,11 @@ func TestSearchRepository(t *testing.T) { opts: &SearchRepoOptions{Keyword: "test", Page: 1, PageSize: 10, OwnerID: 18, Private: true, AllPublic: true}, count: 13}, {name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse}, + opts: &SearchRepoOptions{Page: 1, PageSize: 10, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse}, count: 22}, + {name: "AllTemplates", + opts: &SearchRepoOptions{Page: 1, PageSize: 10, Template: util.OptionalBoolTrue}, + count: 2}, } for _, testCase := range testCases { diff --git a/models/repo_sign.go b/models/repo_sign.go index bac69f76a8..a02b027f89 100644 --- a/models/repo_sign.go +++ b/models/repo_sign.go @@ -149,6 +149,7 @@ func (repo *Repository) SignWikiCommit(u *User) (bool, string) { if err != nil { return false, "" } + defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") if err != nil { return false, "" @@ -194,6 +195,7 @@ func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string if err != nil { return false, "" } + defer gitRepo.Close() commit, err := gitRepo.GetCommit(parentCommit) if err != nil { return false, "" @@ -242,6 +244,7 @@ func (repo *Repository) SignMerge(u *User, tmpBasePath, baseCommit, headCommit s if err != nil { return false, "" } + defer gitRepo.Close() } commit, err := gitRepo.GetCommit(baseCommit) if err != nil { @@ -257,6 +260,7 @@ func (repo *Repository) SignMerge(u *User, tmpBasePath, baseCommit, headCommit s if err != nil { return false, "" } + defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { @@ -272,6 +276,7 @@ func (repo *Repository) SignMerge(u *User, tmpBasePath, baseCommit, headCommit s if err != nil { return false, "" } + defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { diff --git a/models/repo_tag.go b/models/repo_tag.go deleted file mode 100644 index 3864b7a12a..0000000000 --- a/models/repo_tag.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package models - -import ( - "code.gitea.io/gitea/modules/git" -) - -// GetTagsByPath returns repo tags by its path -func GetTagsByPath(path string) ([]*git.Tag, error) { - gitRepo, err := git.OpenRepository(path) - if err != nil { - return nil, err - } - - return gitRepo.GetTagInfos() -} - -// GetTags return repo's tags -func (repo *Repository) GetTags() ([]*git.Tag, error) { - return GetTagsByPath(repo.RepoPath()) -} diff --git a/models/repo_watch.go b/models/repo_watch.go index 53a34efdaf..cb864fb46d 100644 --- a/models/repo_watch.go +++ b/models/repo_watch.go @@ -4,42 +4,118 @@ package models -import "fmt" +import ( + "fmt" + + "code.gitea.io/gitea/modules/setting" +) + +// RepoWatchMode specifies what kind of watch the user has on a repository +type RepoWatchMode int8 + +const ( + // RepoWatchModeNone don't watch + RepoWatchModeNone RepoWatchMode = iota // 0 + // RepoWatchModeNormal watch repository (from other sources) + RepoWatchModeNormal // 1 + // RepoWatchModeDont explicit don't auto-watch + RepoWatchModeDont // 2 + // RepoWatchModeAuto watch repository (from AutoWatchOnChanges) + RepoWatchModeAuto // 3 +) // Watch is connection request for receiving repository notification. type Watch struct { - ID int64 `xorm:"pk autoincr"` - UserID int64 `xorm:"UNIQUE(watch)"` - RepoID int64 `xorm:"UNIQUE(watch)"` + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"UNIQUE(watch)"` + RepoID int64 `xorm:"UNIQUE(watch)"` + Mode RepoWatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"` } -func isWatching(e Engine, userID, repoID int64) bool { - has, _ := e.Get(&Watch{UserID: userID, RepoID: repoID}) - return has +// getWatch gets what kind of subscription a user has on a given repository; returns dummy record if none found +func getWatch(e Engine, userID, repoID int64) (Watch, error) { + watch := Watch{UserID: userID, RepoID: repoID} + has, err := e.Get(&watch) + if err != nil { + return watch, err + } + if !has { + watch.Mode = RepoWatchModeNone + } + return watch, nil +} + +// Decodes watchability of RepoWatchMode +func isWatchMode(mode RepoWatchMode) bool { + return mode != RepoWatchModeNone && mode != RepoWatchModeDont } // IsWatching checks if user has watched given repository. func IsWatching(userID, repoID int64) bool { - return isWatching(x, userID, repoID) + watch, err := getWatch(x, userID, repoID) + return err == nil && isWatchMode(watch.Mode) } -func watchRepo(e Engine, userID, repoID int64, watch bool) (err error) { - if watch { - if isWatching(e, userID, repoID) { - return nil - } - if _, err = e.Insert(&Watch{RepoID: repoID, UserID: userID}); err != nil { +func watchRepoMode(e Engine, watch Watch, mode RepoWatchMode) (err error) { + if watch.Mode == mode { + return nil + } + if mode == RepoWatchModeAuto && (watch.Mode == RepoWatchModeDont || isWatchMode(watch.Mode)) { + // Don't auto watch if already watching or deliberately not watching + return nil + } + + hadrec := watch.Mode != RepoWatchModeNone + needsrec := mode != RepoWatchModeNone + repodiff := 0 + + if isWatchMode(mode) && !isWatchMode(watch.Mode) { + repodiff = 1 + } else if !isWatchMode(mode) && isWatchMode(watch.Mode) { + repodiff = -1 + } + + watch.Mode = mode + + if !hadrec && needsrec { + watch.Mode = mode + if _, err = e.Insert(watch); err != nil { return err } - _, err = e.Exec("UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?", repoID) + } else if needsrec { + watch.Mode = mode + if _, err := e.ID(watch.ID).AllCols().Update(watch); err != nil { + return err + } + } else if _, err = e.Delete(Watch{ID: watch.ID}); err != nil { + return err + } + if repodiff != 0 { + _, err = e.Exec("UPDATE `repository` SET num_watches = num_watches + ? WHERE id = ?", repodiff, watch.RepoID) + } + return err +} + +// WatchRepoMode watch repository in specific mode. +func WatchRepoMode(userID, repoID int64, mode RepoWatchMode) (err error) { + var watch Watch + if watch, err = getWatch(x, userID, repoID); err != nil { + return err + } + return watchRepoMode(x, watch, mode) +} + +func watchRepo(e Engine, userID, repoID int64, doWatch bool) (err error) { + var watch Watch + if watch, err = getWatch(e, userID, repoID); err != nil { + return err + } + if !doWatch && watch.Mode == RepoWatchModeAuto { + err = watchRepoMode(e, watch, RepoWatchModeDont) + } else if !doWatch { + err = watchRepoMode(e, watch, RepoWatchModeNone) } else { - if !isWatching(e, userID, repoID) { - return nil - } - if _, err = e.Delete(&Watch{0, userID, repoID}); err != nil { - return err - } - _, err = e.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repoID) + err = watchRepoMode(e, watch, RepoWatchModeNormal) } return err } @@ -52,6 +128,7 @@ func WatchRepo(userID, repoID int64, watch bool) (err error) { func getWatchers(e Engine, repoID int64) ([]*Watch, error) { watches := make([]*Watch, 0, 10) return watches, e.Where("`watch`.repo_id=?", repoID). + And("`watch`.mode<>?", RepoWatchModeDont). And("`user`.is_active=?", true). And("`user`.prohibit_login=?", false). Join("INNER", "`user`", "`user`.id = `watch`.user_id"). @@ -67,7 +144,8 @@ func GetWatchers(repoID int64) ([]*Watch, error) { func (repo *Repository) GetWatchers(page int) ([]*User, error) { users := make([]*User, 0, ItemsPerPage) sess := x.Where("watch.repo_id=?", repo.ID). - Join("LEFT", "watch", "`user`.id=`watch`.user_id") + Join("LEFT", "watch", "`user`.id=`watch`.user_id"). + And("`watch`.mode<>?", RepoWatchModeDont) if page > 0 { sess = sess.Limit(ItemsPerPage, (page-1)*ItemsPerPage) } @@ -137,3 +215,22 @@ func notifyWatchers(e Engine, act *Action) error { func NotifyWatchers(act *Action) error { return notifyWatchers(x, act) } + +func watchIfAuto(e Engine, userID, repoID int64, isWrite bool) error { + if !isWrite || !setting.Service.AutoWatchOnChanges { + return nil + } + watch, err := getWatch(e, userID, repoID) + if err != nil { + return err + } + if watch.Mode != RepoWatchModeNone { + return nil + } + return watchRepoMode(e, watch, RepoWatchModeAuto) +} + +// WatchIfAuto subscribes to repo if AutoWatchOnChanges is set +func WatchIfAuto(userID int64, repoID int64, isWrite bool) error { + return watchIfAuto(x, userID, repoID, isWrite) +} diff --git a/models/repo_watch_test.go b/models/repo_watch_test.go index 852f09f1c7..c3d40ec919 100644 --- a/models/repo_watch_test.go +++ b/models/repo_watch_test.go @@ -7,6 +7,8 @@ package models import ( "testing" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" ) @@ -15,8 +17,10 @@ func TestIsWatching(t *testing.T) { assert.True(t, IsWatching(1, 1)) assert.True(t, IsWatching(4, 1)) + assert.True(t, IsWatching(11, 1)) assert.False(t, IsWatching(1, 5)) + assert.False(t, IsWatching(8, 1)) assert.False(t, IsWatching(NonexistentID, NonexistentID)) } @@ -78,7 +82,7 @@ func TestNotifyWatchers(t *testing.T) { } assert.NoError(t, NotifyWatchers(action)) - // One watchers are inactive, thus action is only created for user 8, 1, 4 + // One watchers are inactive, thus action is only created for user 8, 1, 4, 11 AssertExistsAndLoadBean(t, &Action{ ActUserID: action.ActUserID, UserID: 8, @@ -97,4 +101,88 @@ func TestNotifyWatchers(t *testing.T) { RepoID: action.RepoID, OpType: action.OpType, }) + AssertExistsAndLoadBean(t, &Action{ + ActUserID: action.ActUserID, + UserID: 11, + RepoID: action.RepoID, + OpType: action.OpType, + }) +} + +func TestWatchIfAuto(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) + watchers, err := repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, repo.NumWatches) + + setting.Service.AutoWatchOnChanges = false + + prevCount := repo.NumWatches + + // Must not add watch + assert.NoError(t, WatchIfAuto(8, 1, true)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Should not add watch + assert.NoError(t, WatchIfAuto(10, 1, true)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + setting.Service.AutoWatchOnChanges = true + + // Must not add watch + assert.NoError(t, WatchIfAuto(8, 1, true)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Should not add watch + assert.NoError(t, WatchIfAuto(12, 1, false)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Should add watch + assert.NoError(t, WatchIfAuto(12, 1, true)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount+1) + + // Should remove watch, inhibit from adding auto + assert.NoError(t, WatchRepo(12, 1, false)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) + + // Must not add watch + assert.NoError(t, WatchIfAuto(12, 1, true)) + watchers, err = repo.GetWatchers(1) + assert.NoError(t, err) + assert.Len(t, watchers, prevCount) +} + +func TestWatchRepoMode(t *testing.T) { + assert.NoError(t, PrepareTestDatabase()) + + AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) + + assert.NoError(t, WatchRepoMode(12, 1, RepoWatchModeAuto)) + AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1) + AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: RepoWatchModeAuto}, 1) + + assert.NoError(t, WatchRepoMode(12, 1, RepoWatchModeNormal)) + AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1) + AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: RepoWatchModeNormal}, 1) + + assert.NoError(t, WatchRepoMode(12, 1, RepoWatchModeDont)) + AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 1) + AssertCount(t, &Watch{UserID: 12, RepoID: 1, Mode: RepoWatchModeDont}, 1) + + assert.NoError(t, WatchRepoMode(12, 1, RepoWatchModeNone)) + AssertCount(t, &Watch{UserID: 12, RepoID: 1}, 0) } diff --git a/models/user.go b/models/user.go index 7aa1e143e8..4a8c644ccd 100644 --- a/models/user.go +++ b/models/user.go @@ -1082,7 +1082,7 @@ func deleteUser(e *xorm.Session, u *User) error { // ***** START: Watch ***** watchedRepoIDs := make([]int64, 0, 10) if err = e.Table("watch").Cols("watch.repo_id"). - Where("watch.user_id = ?", u.ID).Find(&watchedRepoIDs); err != nil { + Where("watch.user_id = ?", u.ID).And("watch.mode <>?", RepoWatchModeDont).Find(&watchedRepoIDs); err != nil { return fmt.Errorf("get all watches: %v", err) } if _, err = e.Decr("num_watches").In("id", watchedRepoIDs).NoAutoTime().Update(new(Repository)); err != nil { @@ -1543,6 +1543,7 @@ func GetStarredRepos(userID int64, private bool) ([]*Repository, error) { // GetWatchedRepos returns the repos watched by a particular user func GetWatchedRepos(userID int64, private bool) ([]*Repository, error) { sess := x.Where("watch.user_id=?", userID). + And("`watch`.mode<>?", RepoWatchModeDont). Join("LEFT", "watch", "`repository`.id=`watch`.repo_id") if !private { sess = sess.And("is_private=?", false) diff --git a/models/user_test.go b/models/user_test.go index bcb955817c..2969e34a76 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -153,7 +153,7 @@ func TestSearchUsers(t *testing.T) { } testUserSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 1}, - []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24}) + []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27}) testUserSuccess(&SearchUserOptions{Page: 1, IsActive: util.OptionalBoolFalse}, []int64{9}) @@ -373,3 +373,16 @@ func TestCreateUser_Issue5882(t *testing.T) { assert.NoError(t, DeleteUser(v.user)) } } + +func TestGetUserIDsByNames(t *testing.T) { + + //ignore non existing + IDs, err := GetUserIDsByNames([]string{"user1", "user2", "none_existing_user"}, true) + assert.NoError(t, err) + assert.Equal(t, []int64{1, 2}, IDs) + + //ignore non existing + IDs, err = GetUserIDsByNames([]string{"user1", "do_not_exist"}, false) + assert.Error(t, err) + assert.Equal(t, []int64(nil), IDs) +} diff --git a/models/wiki.go b/models/wiki.go index 858fe1d6d0..8b63716afa 100644 --- a/models/wiki.go +++ b/models/wiki.go @@ -140,6 +140,7 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con log.Error("Unable to open temporary repository: %s (%v)", basePath, err) return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err) } + defer gitRepo.Close() if hasMasterBranch { if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { @@ -283,6 +284,7 @@ func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error) log.Error("Unable to open temporary repository: %s (%v)", basePath, err) return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err) } + defer gitRepo.Close() if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil { log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err) diff --git a/models/wiki_test.go b/models/wiki_test.go index 991a3d95b9..37c0a86635 100644 --- a/models/wiki_test.go +++ b/models/wiki_test.go @@ -161,6 +161,7 @@ func TestRepository_AddWikiPage(t *testing.T) { // Now need to show that the page has been added: gitRepo, err := git.OpenRepository(repo.WikiPath()) assert.NoError(t, err) + defer gitRepo.Close() masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) wikiPath := WikiNameToFilename(wikiName) @@ -214,6 +215,7 @@ func TestRepository_EditWikiPage(t *testing.T) { _, err := masterTree.GetTreeEntryByPath("Home.md") assert.Error(t, err) } + gitRepo.Close() } } @@ -226,6 +228,7 @@ func TestRepository_DeleteWikiPage(t *testing.T) { // Now need to show that the page has been added: gitRepo, err := git.OpenRepository(repo.WikiPath()) assert.NoError(t, err) + defer gitRepo.Close() masterTree, err := gitRepo.GetTree("master") assert.NoError(t, err) wikiPath := WikiNameToFilename("Home") diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 2280666114..2602dc42eb 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -36,6 +36,10 @@ type CreateRepoForm struct { IssueLabels string License string Readme string + + RepoTemplate int64 + GitContent bool + Topics bool } // Validate validates the fields @@ -107,6 +111,7 @@ type RepoSettingForm struct { MirrorUsername string MirrorPassword string Private bool + Template bool EnablePrune bool // Advanced settings diff --git a/modules/context/api.go b/modules/context/api.go index 024ae487f1..c1de37dd21 100644 --- a/modules/context/api.go +++ b/modules/context/api.go @@ -186,7 +186,16 @@ func ReferencesGitRepo(allowEmpty bool) macaron.Handler { return } ctx.Repo.GitRepo = gitRepo + // We opened it, we should close it + defer func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + }() } + + ctx.Next() } } diff --git a/modules/context/repo.go b/modules/context/repo.go index 8a9c9e4b8c..f41505e7ac 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -189,6 +189,26 @@ func RetrieveBaseRepo(ctx *Context, repo *models.Repository) { } } +// RetrieveTemplateRepo retrieves template repository used to generate this repository +func RetrieveTemplateRepo(ctx *Context, repo *models.Repository) { + // Non-generated repository will not return error in this method. + if err := repo.GetTemplateRepo(); err != nil { + if models.IsErrRepoNotExist(err) { + repo.TemplateID = 0 + return + } + ctx.ServerError("GetTemplateRepo", err) + return + } else if err = repo.TemplateRepo.GetOwner(); err != nil { + ctx.ServerError("TemplateRepo.GetOwner", err) + return + } + + if !repo.TemplateRepo.CheckUnitUser(ctx.User.ID, ctx.User.IsAdmin, models.UnitTypeCode) { + repo.TemplateID = 0 + } +} + // ComposeGoGetImport returns go-get-import meta content. func ComposeGoGetImport(owner, repo string) string { /// setting.AppUrl is guaranteed to be parse as url @@ -414,6 +434,13 @@ func RepoAssignment() macaron.Handler { } } + if repo.IsGenerated() { + RetrieveTemplateRepo(ctx, repo) + if ctx.Written() { + return + } + } + // Disable everything when the repo is being created if ctx.Repo.Repository.IsBeingCreated() { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch @@ -427,9 +454,18 @@ func RepoAssignment() macaron.Handler { } ctx.Repo.GitRepo = gitRepo + // We opened it, we should close it + defer func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + }() + // Stop at this point when the repo is empty. if ctx.Repo.Repository.IsEmpty { ctx.Data["BranchName"] = ctx.Repo.Repository.DefaultBranch + ctx.Next() return } @@ -488,6 +524,7 @@ func RepoAssignment() macaron.Handler { ctx.Data["GoDocDirectory"] = prefix + "{/dir}" ctx.Data["GoDocFile"] = prefix + "{/dir}/{file}#L{line}" } + ctx.Next() } } @@ -518,6 +555,22 @@ func RepoRef() macaron.Handler { return RepoRefByType(RepoRefBranch) } +// RefTypeIncludesBranches returns true if ref type can be a branch +func (rt RepoRefType) RefTypeIncludesBranches() bool { + if rt == RepoRefLegacy || rt == RepoRefAny || rt == RepoRefBranch { + return true + } + return false +} + +// RefTypeIncludesTags returns true if ref type can be a tag +func (rt RepoRefType) RefTypeIncludesTags() bool { + if rt == RepoRefLegacy || rt == RepoRefAny || rt == RepoRefTag { + return true + } + return false +} + func getRefNameFromPath(ctx *Context, path string, isExist func(string) bool) string { refName := "" parts := strings.Split(path, "/") @@ -593,6 +646,13 @@ func RepoRefByType(refType RepoRefType) macaron.Handler { ctx.ServerError("RepoRef Invalid repo "+repoPath, err) return } + // We opened it, we should close it + defer func() { + // If it's been set to nil then assume someone else has closed it. + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + } + }() } // Get default branch. @@ -623,7 +683,7 @@ func RepoRefByType(refType RepoRefType) macaron.Handler { } else { refName = getRefName(ctx, refType) ctx.Repo.BranchName = refName - if ctx.Repo.GitRepo.IsBranchExist(refName) { + if refType.RefTypeIncludesBranches() && ctx.Repo.GitRepo.IsBranchExist(refName) { ctx.Repo.IsViewBranch = true ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(refName) @@ -633,7 +693,7 @@ func RepoRefByType(refType RepoRefType) macaron.Handler { } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if ctx.Repo.GitRepo.IsTagExist(refName) { + } else if refType.RefTypeIncludesTags() && ctx.Repo.GitRepo.IsTagExist(refName) { ctx.Repo.IsViewTag = true ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetTagCommit(refName) if err != nil { @@ -681,6 +741,8 @@ func RepoRefByType(refType RepoRefType) macaron.Handler { return } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount + + ctx.Next() } } diff --git a/routers/api/v1/convert/convert.go b/modules/convert/convert.go similarity index 100% rename from routers/api/v1/convert/convert.go rename to modules/convert/convert.go diff --git a/routers/api/v1/convert/utils.go b/modules/convert/utils.go similarity index 100% rename from routers/api/v1/convert/utils.go rename to modules/convert/utils.go diff --git a/modules/git/blame.go b/modules/git/blame.go index 548236b657..4f4343fe96 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -87,10 +87,11 @@ func (r *BlameReader) Close() error { // CreateBlameReader creates reader for given repository, commit and file func CreateBlameReader(repoPath, commitID, file string) (*BlameReader, error) { - _, err := OpenRepository(repoPath) + gitRepo, err := OpenRepository(repoPath) if err != nil { return nil, err } + gitRepo.Close() return createBlameReader(repoPath, GitExecutable, "blame", commitID, "--porcelain", "--", file) } diff --git a/modules/git/blob_test.go b/modules/git/blob_test.go index 66c046ecc8..9043de5955 100644 --- a/modules/git/blob_test.go +++ b/modules/git/blob_test.go @@ -37,6 +37,8 @@ THE SOFTWARE. ` repo, err := OpenRepository("../../.git") assert.NoError(t, err) + defer repo.Close() + testBlob, err := repo.GetBlob("a8d4b49dd073a4a38a7e58385eeff7cc52568697") assert.NoError(t, err) @@ -55,6 +57,8 @@ func Benchmark_Blob_Data(b *testing.B) { if err != nil { b.Fatal(err) } + defer repo.Close() + testBlob, err := repo.GetBlob("a8d4b49dd073a4a38a7e58385eeff7cc52568697") if err != nil { b.Fatal(err) diff --git a/modules/git/command.go b/modules/git/command.go index 347dcfe39f..7772abd2d5 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "io" + "os" "os/exec" "strings" "time" @@ -24,6 +25,9 @@ var ( DefaultCommandExecutionTimeout = 60 * time.Second ) +// DefaultLocale is the default LC_ALL to run git commands in. +const DefaultLocale = "C" + // Command represents a command with its subcommands or arguments. type Command struct { name string @@ -63,6 +67,13 @@ func (c *Command) RunInDirTimeoutEnvPipeline(env []string, timeout time.Duration // RunInDirTimeoutEnvFullPipeline executes the command in given directory with given timeout, // it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin. func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader) error { + return c.RunInDirTimeoutEnvFullPipelineFunc(env, timeout, dir, stdout, stderr, stdin, nil) +} + +// RunInDirTimeoutEnvFullPipelineFunc executes the command in given directory with given timeout, +// it pipes stdout and stderr to given io.Writer and passes in an io.Reader as stdin. Between cmd.Start and cmd.Wait the passed in function is run. +func (c *Command) RunInDirTimeoutEnvFullPipelineFunc(env []string, timeout time.Duration, dir string, stdout, stderr io.Writer, stdin io.Reader, fn func(context.Context, context.CancelFunc)) error { + if timeout == -1 { timeout = DefaultCommandExecutionTimeout } @@ -77,7 +88,12 @@ func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Dura defer cancel() cmd := exec.CommandContext(ctx, c.name, c.args...) - cmd.Env = env + if env == nil { + cmd.Env = append(os.Environ(), fmt.Sprintf("LC_ALL=%s", DefaultLocale)) + } else { + cmd.Env = env + cmd.Env = append(cmd.Env, fmt.Sprintf("LC_ALL=%s", DefaultLocale)) + } cmd.Dir = dir cmd.Stdout = stdout cmd.Stderr = stderr @@ -89,6 +105,10 @@ func (c *Command) RunInDirTimeoutEnvFullPipeline(env []string, timeout time.Dura pid := process.GetManager().Add(fmt.Sprintf("%s %s %s [repo_path: %s]", GitExecutable, c.name, strings.Join(c.args, " "), dir), cmd) defer process.GetManager().Remove(pid) + if fn != nil { + fn(ctx, cancel) + } + if err := cmd.Wait(); err != nil { return err } diff --git a/modules/git/commit_info_test.go b/modules/git/commit_info_test.go index 71637d188a..ac7bc43c4e 100644 --- a/modules/git/commit_info_test.go +++ b/modules/git/commit_info_test.go @@ -77,6 +77,8 @@ func TestEntries_GetCommitsInfo(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() + testGetCommitsInfo(t, bareRepo1) clonedPath, err := cloneRepo(bareRepo1Path, testReposDir, "repo1_TestEntries_GetCommitsInfo") @@ -84,6 +86,8 @@ func TestEntries_GetCommitsInfo(t *testing.T) { defer os.RemoveAll(clonedPath) clonedRepo1, err := OpenRepository(clonedPath) assert.NoError(t, err) + defer clonedRepo1.Close() + testGetCommitsInfo(t, clonedRepo1) } @@ -101,13 +105,16 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) { for _, benchmark := range benchmarks { var commit *Commit var entries Entries + var repo *Repository if repoPath, err := cloneRepo(benchmark.url, benchmarkReposDir, benchmark.name); err != nil { b.Fatal(err) - } else if repo, err := OpenRepository(repoPath); err != nil { + } else if repo, err = OpenRepository(repoPath); err != nil { b.Fatal(err) } else if commit, err = repo.GetBranchCommit("master"); err != nil { + repo.Close() b.Fatal(err) } else if entries, err = commit.Tree.ListEntries(); err != nil { + repo.Close() b.Fatal(err) } entries.Sort() @@ -120,5 +127,6 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) { } } }) + repo.Close() } } diff --git a/modules/git/notes_test.go b/modules/git/notes_test.go index bf010b9a71..b7939e6913 100644 --- a/modules/git/notes_test.go +++ b/modules/git/notes_test.go @@ -15,6 +15,7 @@ func TestGetNotes(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() note := Note{} err = GetNote(bareRepo1, "95bb4d39648ee7e325106df01a621c530863a653", ¬e) @@ -27,6 +28,7 @@ func TestGetNestedNotes(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo3_notes") repo, err := OpenRepository(repoPath) assert.NoError(t, err) + defer repo.Close() note := Note{} err = GetNote(repo, "3e668dbfac39cbc80a9ff9c61eb565d944453ba4", ¬e) diff --git a/modules/git/repo.go b/modules/git/repo.go index 4c6690b913..ffca2524be 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -17,6 +17,7 @@ import ( "strings" "time" + gitealog "code.gitea.io/gitea/modules/log" "github.com/unknwon/com" "gopkg.in/src-d/go-billy.v4/osfs" gogit "gopkg.in/src-d/go-git.v4" @@ -122,6 +123,16 @@ func OpenRepository(repoPath string) (*Repository, error) { }, nil } +// Close this repository, in particular close the underlying gogitStorage if this is not nil +func (repo *Repository) Close() { + if repo == nil || repo.gogitStorage == nil { + return + } + if err := repo.gogitStorage.Close(); err != nil { + gitealog.Error("Error closing storage: %v", err) + } +} + // GoGitRepo gets the go-git repo representation func (repo *Repository) GoGitRepo() *gogit.Repository { return repo.gogitRepo @@ -304,8 +315,8 @@ const ( statSizeGarbage = "size-garbage: " ) -// GetRepoSize returns disk consumption for repo in path -func GetRepoSize(repoPath string) (*CountObject, error) { +// CountObjects returns the results of git count-objects on the repoPath +func CountObjects(repoPath string) (*CountObject, error) { cmd := NewCommand("count-objects", "-v") stdout, err := cmd.RunInDir(repoPath) if err != nil { diff --git a/modules/git/repo_blob_test.go b/modules/git/repo_blob_test.go index 128a227829..52a124db2a 100644 --- a/modules/git/repo_blob_test.go +++ b/modules/git/repo_blob_test.go @@ -17,6 +17,7 @@ func TestRepository_GetBlob_Found(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo1_bare") r, err := OpenRepository(repoPath) assert.NoError(t, err) + defer r.Close() testCases := []struct { OID string @@ -44,6 +45,7 @@ func TestRepository_GetBlob_NotExist(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo1_bare") r, err := OpenRepository(repoPath) assert.NoError(t, err) + defer r.Close() testCase := "0000000000000000000000000000000000000000" testError := ErrNotExist{testCase, ""} @@ -57,6 +59,7 @@ func TestRepository_GetBlob_NoId(t *testing.T) { repoPath := filepath.Join(testReposDir, "repo1_bare") r, err := OpenRepository(repoPath) assert.NoError(t, err) + defer r.Close() testCase := "" testError := fmt.Errorf("Length must be 40: %s", testCase) diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index a2bf9ac973..e79bab76a6 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -108,6 +108,7 @@ func GetBranchesByPath(path string) ([]*Branch, error) { if err != nil { return nil, err } + defer gitRepo.Close() brs, err := gitRepo.GetBranches() if err != nil { diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index 08736d702e..33d31aef68 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -15,6 +15,7 @@ func TestRepository_GetBranches(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() branches, err := bareRepo1.GetBranches() @@ -29,6 +30,7 @@ func BenchmarkRepository_GetBranches(b *testing.B) { if err != nil { b.Fatal(err) } + defer bareRepo1.Close() for i := 0; i < b.N; i++ { _, err := bareRepo1.GetBranches() diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 6d8ee6453f..87dd6763b3 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -15,6 +15,7 @@ func TestRepository_GetCommitBranches(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() // these test case are specific to the repo1_bare test repo testCases := []struct { @@ -41,6 +42,7 @@ func TestGetTagCommitWithSignature(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() commit, err := bareRepo1.GetCommit("3ad28a9149a2864384548f3d17ed7f38014c9e8a") assert.NoError(t, err) @@ -54,6 +56,7 @@ func TestGetCommitWithBadCommitID(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() commit, err := bareRepo1.GetCommit("bad_branch") assert.Nil(t, commit) diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index e194788773..def67fa87b 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -20,6 +20,7 @@ func TestGetFormatPatch(t *testing.T) { defer os.RemoveAll(clonedPath) repo, err := OpenRepository(clonedPath) assert.NoError(t, err) + defer repo.Close() rd, err := repo.GetFormatPatch("8d92fc95^", "8d92fc95") assert.NoError(t, err) patchb, err := ioutil.ReadAll(rd) diff --git a/modules/git/repo_ref_test.go b/modules/git/repo_ref_test.go index d32b34994c..303c496c1d 100644 --- a/modules/git/repo_ref_test.go +++ b/modules/git/repo_ref_test.go @@ -15,6 +15,7 @@ func TestRepository_GetRefs(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() refs, err := bareRepo1.GetRefs() @@ -38,6 +39,7 @@ func TestRepository_GetRefsFiltered(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() refs, err := bareRepo1.GetRefsFiltered(TagPrefix) diff --git a/modules/git/repo_stats_test.go b/modules/git/repo_stats_test.go index 6fbcb7ac13..bc1f6a5662 100644 --- a/modules/git/repo_stats_test.go +++ b/modules/git/repo_stats_test.go @@ -16,6 +16,7 @@ func TestRepository_GetCodeActivityStats(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() timeFrom, err := time.Parse(time.RFC3339, "2016-01-01T00:00:00+00:00") assert.NoError(t, err) diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index ab9742afc5..90f2b37358 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -16,6 +16,7 @@ func TestRepository_GetTags(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") bareRepo1, err := OpenRepository(bareRepo1Path) assert.NoError(t, err) + defer bareRepo1.Close() tags, err := bareRepo1.GetTagInfos() assert.NoError(t, err) @@ -34,6 +35,7 @@ func TestRepository_GetTag(t *testing.T) { bareRepo1, err := OpenRepository(clonedPath) assert.NoError(t, err) + defer bareRepo1.Close() lTagCommitID := "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1" lTagName := "lightweightTag" @@ -83,6 +85,7 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { bareRepo1, err := OpenRepository(clonedPath) assert.NoError(t, err) + defer bareRepo1.Close() lTagCommitID := "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1" lTagName := "lightweightTag" diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index 15f5e3781c..0b6986764c 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -30,6 +30,7 @@ func TestRepoIsEmpty(t *testing.T) { emptyRepo2Path := filepath.Join(testReposDir, "repo2_empty") repo, err := OpenRepository(emptyRepo2Path) assert.NoError(t, err) + defer repo.Close() isEmpty, err := repo.IsEmpty() assert.NoError(t, err) assert.True(t, isEmpty) diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index c65a691ecf..e872900370 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -56,6 +56,7 @@ func TestEntriesCustomSort(t *testing.T) { func TestFollowLink(t *testing.T) { r, err := OpenRepository("tests/repos/repo1_bare") assert.NoError(t, err) + defer r.Close() commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") assert.NoError(t, err) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index ff78d7ea3a..fc704243e2 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -157,7 +157,8 @@ func RenderRaw(body []byte, urlPrefix string, wikiMarkdown bool) []byte { exts |= blackfriday.HardLineBreak } - body = blackfriday.Run(body, blackfriday.WithRenderer(renderer), blackfriday.WithExtensions(exts)) + // Need to normalize EOL to UNIX LF to have consistent results in rendering + body = blackfriday.Run(util.NormalizeEOL(body), blackfriday.WithRenderer(renderer), blackfriday.WithExtensions(exts)) return markup.SanitizeBytes(body) } diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index b29f870ce5..e80173c6cf 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -294,3 +294,25 @@ func TestTotal_RenderString(t *testing.T) { assert.Equal(t, testCases[i+1], line) } } + +func TestRender_RenderParagraphs(t *testing.T) { + test := func(t *testing.T, str string, cnt int) { + unix := []byte(str) + res := string(RenderRaw(unix, "", false)) + assert.Equal(t, strings.Count(res, "= 39) + assert.True(t, len(milestones) >= 2) for _, milestone := range milestones { switch milestone.Title { case "1.0.0": - assertMilestoneEqual(t, "1.0.0", "2016-12-23 08:00:00 +0000 UTC", - "2016-11-02 18:06:55 +0000 UTC", - "2016-12-29 10:26:00 +0000 UTC", - "2016-12-24 00:40:56 +0000 UTC", + assertMilestoneEqual(t, "Milestone 1.0.0", "1.0.0", "2019-11-11 08:00:00 +0000 UTC", + "2019-11-12 19:37:08 +0000 UTC", + "2019-11-12 21:56:17 +0000 UTC", + "2019-11-12 19:45:49 +0000 UTC", "closed", milestone) case "1.1.0": - assertMilestoneEqual(t, "1.1.0", "2017-02-24 08:00:00 +0000 UTC", - "2016-11-03 08:40:10 +0000 UTC", - "2017-06-15 05:04:36 +0000 UTC", - "2017-03-09 21:22:21 +0000 UTC", - "closed", milestone) - case "1.2.0": - assertMilestoneEqual(t, "1.2.0", "2017-04-24 07:00:00 +0000 UTC", - "2016-11-03 08:40:15 +0000 UTC", - "2017-12-10 02:43:29 +0000 UTC", - "2017-10-12 08:24:28 +0000 UTC", - "closed", milestone) - case "1.3.0": - assertMilestoneEqual(t, "1.3.0", "2017-11-29 08:00:00 +0000 UTC", - "2017-03-03 08:08:59 +0000 UTC", - "2017-12-04 07:48:44 +0000 UTC", - "2017-11-29 18:39:00 +0000 UTC", - "closed", milestone) - case "1.4.0": - assertMilestoneEqual(t, "1.4.0", "2018-01-25 08:00:00 +0000 UTC", - "2017-08-23 11:02:37 +0000 UTC", - "2018-03-25 20:01:56 +0000 UTC", - "2018-03-25 20:01:56 +0000 UTC", - "closed", milestone) - case "1.5.0": - assertMilestoneEqual(t, "1.5.0", "2018-06-15 07:00:00 +0000 UTC", - "2017-12-30 04:21:56 +0000 UTC", - "2018-09-05 16:34:22 +0000 UTC", - "2018-08-11 08:45:01 +0000 UTC", - "closed", milestone) - case "1.7.0": - assertMilestoneEqual(t, "1.7.0", "2018-12-25 08:00:00 +0000 UTC", - "2018-08-28 14:20:14 +0000 UTC", - "2019-01-27 11:30:24 +0000 UTC", - "2019-01-23 08:58:23 +0000 UTC", + assertMilestoneEqual(t, "Milestone 1.1.0", "1.1.0", "2019-11-12 08:00:00 +0000 UTC", + "2019-11-12 19:37:25 +0000 UTC", + "2019-11-12 21:39:27 +0000 UTC", + "2019-11-12 19:45:46 +0000 UTC", "closed", milestone) } } labels, err := downloader.GetLabels() assert.NoError(t, err) - assert.True(t, len(labels) >= 48) + assert.True(t, len(labels) >= 8) for _, l := range labels { switch l.Name { - case "backport/v1.7": - assertLabelEqual(t, "backport/v1.7", "fbca04", l) - case "backport/v1.8": - assertLabelEqual(t, "backport/v1.8", "fbca04", l) - case "kind/api": - assertLabelEqual(t, "kind/api", "5319e7", l) - case "kind/breaking": - assertLabelEqual(t, "kind/breaking", "fbca04", l) - case "kind/bug": - assertLabelEqual(t, "kind/bug", "ee0701", l) - case "kind/docs": - assertLabelEqual(t, "kind/docs", "c2e0c6", l) - case "kind/enhancement": - assertLabelEqual(t, "kind/enhancement", "84b6eb", l) - case "kind/feature": - assertLabelEqual(t, "kind/feature", "006b75", l) + case "bug": + assertLabelEqual(t, "bug", "d73a4a", "Something isn't working", l) + case "documentation": + assertLabelEqual(t, "documentation", "0075ca", "Improvements or additions to documentation", l) + case "duplicate": + assertLabelEqual(t, "duplicate", "cfd3d7", "This issue or pull request already exists", l) + case "enhancement": + assertLabelEqual(t, "enhancement", "a2eeef", "New feature or request", l) + case "good first issue": + assertLabelEqual(t, "good first issue", "7057ff", "Good for newcomers", l) + case "help wanted": + assertLabelEqual(t, "help wanted", "008672", "Extra attention is needed", l) + case "invalid": + assertLabelEqual(t, "invalid", "e4e669", "This doesn't seem right", l) + case "question": + assertLabelEqual(t, "question", "d876e3", "Further information is requested", l) } } @@ -157,48 +128,50 @@ func TestGitHubDownloadRepo(t *testing.T) { { TagName: "v0.9.99", TargetCommitish: "master", - Name: "fork", - Body: "Forked source from Gogs into Gitea\n", - Created: time.Date(2016, 10, 17, 02, 17, 59, 0, time.UTC), - Published: time.Date(2016, 11, 17, 15, 37, 0, 0, time.UTC), - PublisherID: 4726179, - PublisherName: "bkcsoft", + Name: "First Release", + Body: "A test release", + Created: time.Date(2019, 11, 9, 16, 49, 21, 0, time.UTC), + Published: time.Date(2019, 11, 12, 20, 12, 10, 0, time.UTC), + PublisherID: 1669571, + PublisherName: "mrsdizzie", }, }, releases[len(releases)-1:]) // downloader.GetIssues() - issues, isEnd, err := downloader.GetIssues(1, 8) + issues, isEnd, err := downloader.GetIssues(1, 2) assert.NoError(t, err) - assert.EqualValues(t, 3, len(issues)) + assert.EqualValues(t, 2, len(issues)) assert.False(t, isEnd) var ( - closed1 = time.Date(2018, 10, 23, 02, 57, 43, 0, time.UTC) - closed7 = time.Date(2019, 7, 8, 8, 20, 23, 0, time.UTC) + closed1 = time.Date(2019, 11, 12, 20, 22, 22, 0, time.UTC) + closed2 = time.Date(2019, 11, 12, 21, 1, 31, 0, time.UTC) ) assert.EqualValues(t, []*base.Issue{ { - Number: 6, - Title: "Contribution system: History heatmap for user", - Content: "Hi guys,\r\n\r\nI think that is a possible feature, a history heatmap similar to github or gitlab.\r\nActually exists a plugin called Calendar HeatMap. I used this on mine project to heat application log and worked fine here.\r\nThen, is only a idea, what you think? :)\r\n\r\nhttp://cal-heatmap.com/\r\nhttps://github.com/wa0x6e/cal-heatmap\r\n\r\nReference: https://github.com/gogits/gogs/issues/1640", - Milestone: "1.7.0", - PosterID: 1520407, - PosterName: "joubertredrat", + Number: 1, + Title: "Please add an animated gif icon to the merge button", + Content: "I just want the merge button to hurt my eyes a little. \xF0\x9F\x98\x9D ", + Milestone: "1.0.0", + PosterID: 18600385, + PosterName: "guillep2k", State: "closed", - Created: time.Date(2016, 11, 02, 18, 51, 55, 0, time.UTC), + Created: time.Date(2019, 11, 9, 17, 0, 29, 0, time.UTC), Labels: []*base.Label{ { - Name: "kind/feature", - Color: "006b75", + Name: "bug", + Color: "d73a4a", + Description: "Something isn't working", }, { - Name: "kind/ui", - Color: "fef2c0", + Name: "good first issue", + Color: "7057ff", + Description: "Good for newcomers", }, }, Reactions: &base.Reactions{ - TotalCount: 0, - PlusOne: 0, + TotalCount: 1, + PlusOne: 1, MinusOne: 0, Laugh: 0, Confused: 0, @@ -208,84 +181,48 @@ func TestGitHubDownloadRepo(t *testing.T) { Closed: &closed1, }, { - Number: 7, - Title: "display page revisions on wiki", - Content: "Hi guys,\r\n\r\nWiki on Gogs is very fine, I liked a lot, but I think that is good idea to be possible see other revisions from page as a page history.\r\n\r\nWhat you think?\r\n\r\nReference: https://github.com/gogits/gogs/issues/2991", - Milestone: "1.10.0", - PosterID: 1520407, - PosterName: "joubertredrat", + Number: 2, + Title: "Test issue", + Content: "This is test issue 2, do not touch!", + Milestone: "1.1.0", + PosterID: 1669571, + PosterName: "mrsdizzie", State: "closed", - Created: time.Date(2016, 11, 02, 18, 57, 32, 0, time.UTC), + Created: time.Date(2019, 11, 12, 21, 0, 6, 0, time.UTC), Labels: []*base.Label{ { - Name: "kind/feature", - Color: "006b75", - }, - { - Name: "reviewed/confirmed", - Color: "8d9b12", - Description: "Issue has been reviewed and confirmed to be present or accepted to be implemented", + Name: "duplicate", + Color: "cfd3d7", + Description: "This issue or pull request already exists", }, }, Reactions: &base.Reactions{ TotalCount: 6, - PlusOne: 5, - MinusOne: 0, - Laugh: 0, + PlusOne: 1, + MinusOne: 1, + Laugh: 1, Confused: 1, - Heart: 0, - Hooray: 0, - }, - Closed: &closed7, - }, - { - Number: 8, - Title: "audit logs", - Content: "Hi,\r\n\r\nI think that is good idea to have user operation log to admin see what the user is doing at Gogs. Similar to example below\r\n\r\n| user | operation | information |\r\n| --- | --- | --- |\r\n| joubertredrat | repo.create | Create repo MyProjectData |\r\n| joubertredrat | user.settings | Edit settings |\r\n| tboerger | repo.fork | Create Fork from MyProjectData to ForkMyProjectData |\r\n| bkcsoft | repo.remove | Remove repo MySource |\r\n| tboerger | admin.auth | Edit auth LDAP org-connection |\r\n\r\nThis resource can be used on user page too, as user activity, set that log row is public (repo._) or private (user._, admin.*) and display only public activity.\r\n\r\nWhat you think?\r\n\r\n[Chat summary from March 14, 2017](https://github.com/go-gitea/gitea/issues/8#issuecomment-286463807)\r\n\r\nReferences:\r\nhttps://github.com/gogits/gogs/issues/3016", - Milestone: "1.x.x", - PosterID: 1520407, - PosterName: "joubertredrat", - State: "open", - Created: time.Date(2016, 11, 02, 18, 59, 20, 0, time.UTC), - Labels: []*base.Label{ - { - Name: "kind/feature", - Color: "006b75", - }, - { - Name: "kind/proposal", - Color: "5319e7", - }, - }, - Reactions: &base.Reactions{ - TotalCount: 9, - PlusOne: 8, - MinusOne: 0, - Laugh: 0, - Confused: 0, Heart: 1, - Hooray: 0, + Hooray: 1, }, + Closed: &closed2, }, }, issues) // downloader.GetComments() - comments, err := downloader.GetComments(6) + comments, err := downloader.GetComments(2) assert.NoError(t, err) - assert.EqualValues(t, 35, len(comments)) + assert.EqualValues(t, 2, len(comments)) assert.EqualValues(t, []*base.Comment{ { - IssueIndex: 6, - PosterID: 4726179, - PosterName: "bkcsoft", - Created: time.Date(2016, 11, 02, 18, 59, 48, 0, time.UTC), - Content: `I would prefer a solution that is in the backend, unless it's required to have it update without reloading. Unfortunately I can't seem to find anything that does that :unamused: - -Also this would _require_ caching, since it will fetch huge amounts of data from disk... -`, + IssueIndex: 2, + PosterID: 1669571, + PosterName: "mrsdizzie", + Created: time.Date(2019, 11, 12, 21, 0, 13, 0, time.UTC), + Content: "This is a comment", Reactions: &base.Reactions{ - TotalCount: 2, - PlusOne: 2, + TotalCount: 1, + PlusOne: 1, MinusOne: 0, Laugh: 0, Confused: 0, @@ -294,14 +231,11 @@ Also this would _require_ caching, since it will fetch huge amounts of data from }, }, { - IssueIndex: 6, - PosterID: 1520407, - PosterName: "joubertredrat", - Created: time.Date(2016, 11, 02, 19, 16, 56, 0, time.UTC), - Content: `Yes, this plugin build on front-end, with backend I don't know too, but we can consider make component for this. - -In my case I use ajax to get data, but build on frontend anyway -`, + IssueIndex: 2, + PosterID: 1669571, + PosterName: "mrsdizzie", + Created: time.Date(2019, 11, 12, 22, 7, 14, 0, time.UTC), + Content: "A second comment", Reactions: &base.Reactions{ TotalCount: 0, PlusOne: 0, @@ -312,154 +246,85 @@ In my case I use ajax to get data, but build on frontend anyway Hooray: 0, }, }, - { - IssueIndex: 6, - PosterID: 1799009, - PosterName: "xinity", - Created: time.Date(2016, 11, 03, 13, 04, 56, 0, time.UTC), - Content: `following @bkcsoft retention strategy in cache is a must if we don't want gitea to waste ressources. -something like in the latest 15days could be enough don't you think ? -`, - Reactions: &base.Reactions{ - TotalCount: 2, - PlusOne: 2, - MinusOne: 0, - Laugh: 0, - Confused: 0, - Heart: 0, - Hooray: 0, - }, - }, - }, comments[:3]) + }, comments[:2]) // downloader.GetPullRequests() - prs, err := downloader.GetPullRequests(1, 3) + prs, err := downloader.GetPullRequests(1, 2) assert.NoError(t, err) - assert.EqualValues(t, 3, len(prs)) + assert.EqualValues(t, 2, len(prs)) - closed1 = time.Date(2016, 11, 02, 18, 22, 21, 0, time.UTC) - var ( - closed2 = time.Date(2016, 11, 03, 8, 06, 27, 0, time.UTC) - closed3 = time.Date(2016, 11, 02, 18, 22, 31, 0, time.UTC) - ) + closed1 = time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC) + var merged1 = time.Date(2019, 11, 12, 21, 39, 27, 0, time.UTC) - var ( - merged1 = time.Date(2016, 11, 02, 18, 22, 21, 0, time.UTC) - merged2 = time.Date(2016, 11, 03, 8, 06, 27, 0, time.UTC) - merged3 = time.Date(2016, 11, 02, 18, 22, 31, 0, time.UTC) - ) assert.EqualValues(t, []*base.PullRequest{ { - Number: 1, - Title: "Rename import paths: \"github.com/gogits/gogs\" -> \"github.com/go-gitea/gitea\"", - Content: "", - Milestone: "1.0.0", - PosterID: 7011819, - PosterName: "andreynering", + Number: 3, + Title: "Update README.md", + Content: "add warning to readme", + Milestone: "1.1.0", + PosterID: 1669571, + PosterName: "mrsdizzie", State: "closed", - Created: time.Date(2016, 11, 02, 17, 01, 19, 0, time.UTC), + Created: time.Date(2019, 11, 12, 21, 21, 43, 0, time.UTC), Labels: []*base.Label{ { - Name: "kind/enhancement", - Color: "84b6eb", - }, - { - Name: "lgtm/done", - Color: "0e8a16", + Name: "documentation", + Color: "0075ca", + Description: "Improvements or additions to documentation", }, }, - PatchURL: "https://github.com/go-gitea/gitea/pull/1.patch", + PatchURL: "https://github.com/go-gitea/test_repo/pull/3.patch", Head: base.PullRequestBranch{ - Ref: "import-paths", - SHA: "1b0ec3208db8501acba44a137c009a5a126ebaa9", - OwnerName: "andreynering", + Ref: "master", + CloneURL: "https://github.com/mrsdizzie/test_repo.git", + SHA: "076160cf0b039f13e5eff19619932d181269414b", + RepoName: "test_repo", + + OwnerName: "mrsdizzie", }, Base: base.PullRequestBranch{ Ref: "master", - SHA: "6bcff7828f117af8d51285ce3acba01a7e40a867", + SHA: "72866af952e98d02a73003501836074b286a78f6", OwnerName: "go-gitea", - RepoName: "gitea", + RepoName: "test_repo", }, Closed: &closed1, Merged: true, MergedTime: &merged1, - MergeCommitSHA: "142d35e8d2baec230ddb565d1265940d59141fab", + MergeCommitSHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2", }, { - Number: 2, - Title: "Fix sender of issue notifications", - Content: "It is the FROM field in mailer configuration that needs be used,\r\nnot the USER field, which is for authentication.\r\n\r\nMigrated from https://github.com/gogits/gogs/pull/3616\r\n", + Number: 4, + Title: "Test branch", + Content: "do not merge this PR", Milestone: "1.0.0", - PosterID: 289678, - PosterName: "strk", - State: "closed", - Created: time.Date(2016, 11, 02, 17, 24, 19, 0, time.UTC), + PosterID: 1669571, + PosterName: "mrsdizzie", + State: "open", + Created: time.Date(2019, 11, 12, 21, 54, 18, 0, time.UTC), Labels: []*base.Label{ { - Name: "kind/bug", - Color: "ee0701", - }, - { - Name: "lgtm/done", - Color: "0e8a16", + Name: "bug", + Color: "d73a4a", + Description: "Something isn't working", }, }, - PatchURL: "https://github.com/go-gitea/gitea/pull/2.patch", + PatchURL: "https://github.com/go-gitea/test_repo/pull/4.patch", Head: base.PullRequestBranch{ - Ref: "proper-from-on-issue-mail", - SHA: "af03d00780a6ee70c58e135c6679542cde4f8d50", - RepoName: "gogs", - OwnerName: "strk", - CloneURL: "https://github.com/strk/gogs.git", + Ref: "test-branch", + SHA: "2be9101c543658591222acbee3eb799edfc3853d", + RepoName: "test_repo", + OwnerName: "mrsdizzie", + CloneURL: "https://github.com/mrsdizzie/test_repo.git", }, Base: base.PullRequestBranch{ - Ref: "develop", - SHA: "5c5424301443ffa3659737d12de48ab1dfe39a00", + Ref: "master", + SHA: "f32b0a9dfd09a60f616f29158f772cedd89942d2", OwnerName: "go-gitea", - RepoName: "gitea", + RepoName: "test_repo", }, - Closed: &closed2, - Merged: true, - MergedTime: &merged2, - MergeCommitSHA: "d8de2beb5b92d02a0597ba7c7803839380666653", - }, - { - Number: 3, - Title: "Use proper url for libravatar dep", - Content: "Fetch go-libravatar from its official source, rather than from an unmaintained fork\r\n", - Milestone: "1.0.0", - PosterID: 289678, - PosterName: "strk", - State: "closed", - Created: time.Date(2016, 11, 02, 17, 34, 31, 0, time.UTC), - Labels: []*base.Label{ - { - Name: "kind/enhancement", - Color: "84b6eb", - }, - { - Name: "lgtm/done", - Color: "0e8a16", - }, - }, - PatchURL: "https://github.com/go-gitea/gitea/pull/3.patch", - Head: base.PullRequestBranch{ - Ref: "libravatar-proper-url", - SHA: "d59a48a2550abd4129b96d38473941b895a4859b", - RepoName: "gogs", - OwnerName: "strk", - CloneURL: "https://github.com/strk/gogs.git", - }, - Base: base.PullRequestBranch{ - Ref: "develop", - SHA: "6bcff7828f117af8d51285ce3acba01a7e40a867", - OwnerName: "go-gitea", - RepoName: "gitea", - }, - Closed: &closed3, - Merged: true, - MergedTime: &merged3, - MergeCommitSHA: "5c5424301443ffa3659737d12de48ab1dfe39a00", + Merged: false, + MergeCommitSHA: "565d1208f5fffdc1c5ae1a2436491eb9a5e4ebae", }, }, prs) } diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index bbc1dc2d56..7a5071e125 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -94,6 +94,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts if err := uploader.CreateRepo(repo, opts); err != nil { return err } + defer uploader.Close() log.Trace("migrating topics") topics, err := downloader.GetTopics() diff --git a/modules/notification/action/action.go b/modules/notification/action/action.go index 52471c1107..d481bd8c4d 100644 --- a/modules/notification/action/action.go +++ b/modules/notification/action/action.go @@ -91,3 +91,29 @@ func (a *actionNotifier) NotifyRenameRepository(doer *models.User, repo *models. log.Trace("action.renameRepoAction: %s/%s", doer.Name, repo.Name) } } + +func (a *actionNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { + if err := models.NotifyWatchers(&models.Action{ + ActUserID: doer.ID, + ActUser: doer, + OpType: models.ActionCreateRepo, + RepoID: repo.ID, + Repo: repo, + IsPrivate: repo.IsPrivate, + }); err != nil { + log.Error("notify watchers '%d/%d': %v", doer.ID, repo.ID, err) + } +} + +func (a *actionNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo *models.Repository) { + if err := models.NotifyWatchers(&models.Action{ + ActUserID: doer.ID, + ActUser: doer, + OpType: models.ActionCreateRepo, + RepoID: repo.ID, + Repo: repo, + IsPrivate: repo.IsPrivate, + }); err != nil { + log.Error("notify watchers '%d/%d': %v", doer.ID, repo.ID, err) + } +} diff --git a/modules/notification/ui/ui.go b/modules/notification/ui/ui.go index 22089158f5..bfe497f866 100644 --- a/modules/notification/ui/ui.go +++ b/modules/notification/ui/ui.go @@ -18,7 +18,8 @@ type ( } issueNotificationOpts struct { - issue *models.Issue + issueID int64 + commentID int64 notificationAuthorID int64 } ) @@ -36,7 +37,7 @@ func NewNotifier() base.Notifier { func (ns *notificationService) Run() { for opts := range ns.issueQueue { - if err := models.CreateOrUpdateIssueNotifications(opts.issue, opts.notificationAuthorID); err != nil { + if err := models.CreateOrUpdateIssueNotifications(opts.issueID, opts.commentID, opts.notificationAuthorID); err != nil { log.Error("Was unable to create issue notification: %v", err) } } @@ -44,43 +45,51 @@ func (ns *notificationService) Run() { func (ns *notificationService) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, issue *models.Issue, comment *models.Comment) { - ns.issueQueue <- issueNotificationOpts{ - issue, - doer.ID, + var opts = issueNotificationOpts{ + issueID: issue.ID, + notificationAuthorID: doer.ID, } + if comment != nil { + opts.commentID = comment.ID + } + ns.issueQueue <- opts } func (ns *notificationService) NotifyNewIssue(issue *models.Issue) { ns.issueQueue <- issueNotificationOpts{ - issue, - issue.Poster.ID, + issueID: issue.ID, + notificationAuthorID: issue.Poster.ID, } } func (ns *notificationService) NotifyIssueChangeStatus(doer *models.User, issue *models.Issue, isClosed bool) { ns.issueQueue <- issueNotificationOpts{ - issue, - doer.ID, + issueID: issue.ID, + notificationAuthorID: doer.ID, } } func (ns *notificationService) NotifyMergePullRequest(pr *models.PullRequest, doer *models.User, gitRepo *git.Repository) { ns.issueQueue <- issueNotificationOpts{ - pr.Issue, - doer.ID, + issueID: pr.Issue.ID, + notificationAuthorID: doer.ID, } } func (ns *notificationService) NotifyNewPullRequest(pr *models.PullRequest) { ns.issueQueue <- issueNotificationOpts{ - pr.Issue, - pr.Issue.PosterID, + issueID: pr.Issue.ID, + notificationAuthorID: pr.Issue.PosterID, } } func (ns *notificationService) NotifyPullRequestReview(pr *models.PullRequest, r *models.Review, c *models.Comment) { - ns.issueQueue <- issueNotificationOpts{ - pr.Issue, - r.Reviewer.ID, + var opts = issueNotificationOpts{ + issueID: pr.Issue.ID, + notificationAuthorID: r.Reviewer.ID, } + if c != nil { + opts.commentID = c.ID + } + ns.issueQueue <- opts } diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 8059ec1c00..43be0d2e1c 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -573,11 +573,13 @@ func (m *webhookNotifier) NotifyCreateRef(pusher *models.User, repo *models.Repo return } - shaSum, err := gitRepo.GetBranchCommitID(refName) + shaSum, err := gitRepo.GetRefCommitID(refFullName) if err != nil { - log.Error("GetBranchCommitID[%s]: %v", refFullName, err) + gitRepo.Close() + log.Error("GetRefCommitID[%s]: %v", refFullName, err) return } + gitRepo.Close() if err = webhook_module.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ Ref: refName, diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go index 996363863d..8b35cba726 100644 --- a/modules/repofiles/action.go +++ b/modules/repofiles/action.go @@ -53,9 +53,11 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { } if err := gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { if !git.IsErrUnsupportedVersion(err) { + gitRepo.Close() return err } } + gitRepo.Close() } } diff --git a/modules/repofiles/blob.go b/modules/repofiles/blob.go index e9d85a0dcf..60a05e280e 100644 --- a/modules/repofiles/blob.go +++ b/modules/repofiles/blob.go @@ -17,6 +17,7 @@ func GetBlobBySHA(repo *models.Repository, sha string) (*api.GitBlobResponse, er if err != nil { return nil, err } + defer gitRepo.Close() gitBlob, err := gitRepo.GetBlob(sha) if err != nil { return nil, err diff --git a/modules/repofiles/blob_test.go b/modules/repofiles/blob_test.go index 1dc183a8af..ddc23aeac3 100644 --- a/modules/repofiles/blob_test.go +++ b/modules/repofiles/blob_test.go @@ -21,6 +21,8 @@ func TestGetBlobBySHA(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" ctx.SetParams(":id", "1") ctx.SetParams(":sha", sha) diff --git a/modules/repofiles/commit_status.go b/modules/repofiles/commit_status.go index f3dfbf209f..3d93c58d85 100644 --- a/modules/repofiles/commit_status.go +++ b/modules/repofiles/commit_status.go @@ -23,8 +23,10 @@ func CreateCommitStatus(repo *models.Repository, creator *models.User, sha strin return fmt.Errorf("OpenRepository[%s]: %v", repoPath, err) } if _, err := gitRepo.GetCommit(sha); err != nil { + gitRepo.Close() return fmt.Errorf("GetCommit[%s]: %v", sha, err) } + gitRepo.Close() if err := models.NewCommitStatus(models.NewCommitStatusOptions{ Repo: repo, diff --git a/modules/repofiles/content.go b/modules/repofiles/content.go index d7d43ef9d1..aed98c33a8 100644 --- a/modules/repofiles/content.go +++ b/modules/repofiles/content.go @@ -59,6 +59,7 @@ func GetContentsOrList(repo *models.Repository, treePath, ref string) (interface if err != nil { return nil, err } + defer gitRepo.Close() // Get the commit object for the ref commit, err := gitRepo.GetCommit(ref) @@ -117,6 +118,7 @@ func GetContents(repo *models.Repository, treePath, ref string, forList bool) (* if err != nil { return nil, err } + defer gitRepo.Close() // Get the commit object for the ref commit, err := gitRepo.GetCommit(ref) diff --git a/modules/repofiles/content_test.go b/modules/repofiles/content_test.go index cd98c54ea6..d024cfd549 100644 --- a/modules/repofiles/content_test.go +++ b/modules/repofiles/content_test.go @@ -56,6 +56,8 @@ func TestGetContents(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + treePath := "README.md" ref := ctx.Repo.Repository.DefaultBranch @@ -82,6 +84,8 @@ func TestGetContentsOrListForDir(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + treePath := "" // root dir ref := ctx.Repo.Repository.DefaultBranch @@ -115,6 +119,8 @@ func TestGetContentsOrListForFile(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + treePath := "README.md" ref := ctx.Repo.Repository.DefaultBranch @@ -141,6 +147,8 @@ func TestGetContentsErrors(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository treePath := "README.md" ref := repo.DefaultBranch @@ -170,6 +178,8 @@ func TestGetContentsOrListErrors(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository treePath := "README.md" ref := repo.DefaultBranch @@ -198,6 +208,8 @@ func TestGetContentsOrListOfEmptyRepos(t *testing.T) { test.LoadRepo(t, ctx, 15) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository t.Run("empty repo", func(t *testing.T) { diff --git a/modules/repofiles/diff_test.go b/modules/repofiles/diff_test.go index de5ed1d754..db2c7552c4 100644 --- a/modules/repofiles/diff_test.go +++ b/modules/repofiles/diff_test.go @@ -22,6 +22,8 @@ func TestGetDiffPreview(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + branch := ctx.Repo.Repository.DefaultBranch treePath := "README.md" content := "# repo1\n\nDescription for repo1\nthis is a new line" @@ -119,6 +121,8 @@ func TestGetDiffPreviewErrors(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + branch := ctx.Repo.Repository.DefaultBranch treePath := "README.md" content := "# repo1\n\nDescription for repo1\nthis is a new line" diff --git a/modules/repofiles/file_test.go b/modules/repofiles/file_test.go index 95ec175ed4..3cb4eb472b 100644 --- a/modules/repofiles/file_test.go +++ b/modules/repofiles/file_test.go @@ -88,10 +88,13 @@ func TestGetFileResponseFromCommit(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository branch := repo.DefaultBranch treePath := "README.md" gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() commit, _ := gitRepo.GetBranchCommit(branch) expectedFileResponse := getExpectedFileResponse() diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index b07d2a8973..6bd775d9d2 100644 --- a/modules/repofiles/temp_repo.go +++ b/modules/repofiles/temp_repo.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "os" - "os/exec" "regexp" "strings" "time" @@ -18,7 +17,6 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/gitdiff" @@ -44,6 +42,7 @@ func NewTemporaryUploadRepository(repo *models.Repository) (*TemporaryUploadRepo // Close the repository cleaning up all files func (t *TemporaryUploadRepository) Close() { + defer t.gitRepo.Close() if err := models.RemoveTemporaryPath(t.basePath); err != nil { log.Error("Failed to remove temporary path %s: %v", t.basePath, err) } @@ -51,9 +50,8 @@ func (t *TemporaryUploadRepository) Close() { // Clone the base repository to our path and set branch as the HEAD func (t *TemporaryUploadRepository) Clone(branch string) error { - if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute, - fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath), - git.GitExecutable, "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil { + if _, err := git.NewCommand("clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath).Run(); err != nil { + stderr := err.Error() if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched { return git.ErrBranchNotExist{ Name: branch, @@ -79,11 +77,8 @@ func (t *TemporaryUploadRepository) Clone(branch string) error { // SetDefaultIndex sets the git index to our HEAD func (t *TemporaryUploadRepository) SetDefaultIndex() error { - if _, stderr, err := process.GetManager().ExecDir(5*time.Minute, - t.basePath, - fmt.Sprintf("SetDefaultIndex (git read-tree HEAD): %s", t.basePath), - git.GitExecutable, "read-tree", "HEAD"); err != nil { - return fmt.Errorf("SetDefaultIndex: %v %s", err, stderr) + if _, err := git.NewCommand("read-tree", "HEAD").RunInDir(t.basePath); err != nil { + return fmt.Errorf("SetDefaultIndex: %v", err) } return nil } @@ -93,10 +88,6 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro stdOut := new(bytes.Buffer) stdErr := new(bytes.Buffer) - timeout := 5 * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - cmdArgs := []string{"ls-files", "-z", "--"} for _, arg := range filenames { if arg != "" { @@ -104,22 +95,9 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro } } - cmd := exec.CommandContext(ctx, git.GitExecutable, cmdArgs...) - desc := fmt.Sprintf("lsFiles: (git ls-files) %v", cmdArgs) - cmd.Dir = t.basePath - cmd.Stdout = stdOut - cmd.Stderr = stdErr - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err()) - } - - pid := process.GetManager().Add(desc, cmd) - err := cmd.Wait() - process.GetManager().Remove(pid) - - if err != nil { - err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr) + if err := git.NewCommand(cmdArgs...).RunInDirPipeline(t.basePath, stdOut, stdErr); err != nil { + log.Error("Unable to run git ls-files for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String()) + err = fmt.Errorf("Unable to run git ls-files for temporary repo of: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String()) return nil, err } @@ -128,7 +106,7 @@ func (t *TemporaryUploadRepository) LsFiles(filenames ...string) ([]string, erro filelist = append(filelist, string(line)) } - return filelist, err + return filelist, nil } // RemoveFilesFromIndex removes the given files from the index @@ -144,90 +122,50 @@ func (t *TemporaryUploadRepository) RemoveFilesFromIndex(filenames ...string) er } } - timeout := 5 * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - cmdArgs := []string{"update-index", "--remove", "-z", "--index-info"} - cmd := exec.CommandContext(ctx, git.GitExecutable, cmdArgs...) - desc := fmt.Sprintf("removeFilesFromIndex: (git update-index) %v", filenames) - cmd.Dir = t.basePath - cmd.Stdout = stdOut - cmd.Stderr = stdErr - cmd.Stdin = bytes.NewReader(stdIn.Bytes()) - - if err := cmd.Start(); err != nil { - return fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err()) + if err := git.NewCommand("update-index", "--remove", "-z", "--index-info").RunInDirFullPipeline(t.basePath, stdOut, stdErr, stdIn); err != nil { + log.Error("Unable to update-index for temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String()) + return fmt.Errorf("Unable to update-index for temporary repo: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String()) } - - pid := process.GetManager().Add(desc, cmd) - err := cmd.Wait() - process.GetManager().Remove(pid) - - if err != nil { - err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr) - } - - return err + return nil } // HashObject writes the provided content to the object db and returns its hash func (t *TemporaryUploadRepository) HashObject(content io.Reader) (string, error) { - timeout := 5 * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() + stdOut := new(bytes.Buffer) + stdErr := new(bytes.Buffer) - hashCmd := exec.CommandContext(ctx, git.GitExecutable, "hash-object", "-w", "--stdin") - hashCmd.Dir = t.basePath - hashCmd.Stdin = content - stdOutBuffer := new(bytes.Buffer) - stdErrBuffer := new(bytes.Buffer) - hashCmd.Stdout = stdOutBuffer - hashCmd.Stderr = stdErrBuffer - desc := fmt.Sprintf("hashObject: (git hash-object)") - if err := hashCmd.Start(); err != nil { - return "", fmt.Errorf("git hash-object: %s", err) + if err := git.NewCommand("hash-object", "-w", "--stdin").RunInDirFullPipeline(t.basePath, stdOut, stdErr, content); err != nil { + log.Error("Unable to hash-object to temporary repo: %s (%s) Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), t.basePath, err, stdOut.String(), stdErr.String()) + return "", fmt.Errorf("Unable to hash-object to temporary repo: %s Error: %v\nstdout: %s\nstderr: %s", t.repo.FullName(), err, stdOut.String(), stdErr.String()) } - pid := process.GetManager().Add(desc, hashCmd) - err := hashCmd.Wait() - process.GetManager().Remove(pid) - - if err != nil { - err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOutBuffer, stdErrBuffer) - return "", err - } - - return strings.TrimSpace(stdOutBuffer.String()), nil + return strings.TrimSpace(stdOut.String()), nil } // AddObjectToIndex adds the provided object hash to the index with the provided mode and path func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPath string) error { - if _, stderr, err := process.GetManager().ExecDir(5*time.Minute, - t.basePath, - fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath), - git.GitExecutable, "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil { + if _, err := git.NewCommand("update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath).RunInDir(t.basePath); err != nil { + stderr := err.Error() if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched { return models.ErrFilePathInvalid{ Message: objectPath, Path: objectPath, } } - return fmt.Errorf("git update-index: %s", stderr) + log.Error("Unable to add object to index: %s %s %s in temporary repo %s(%s) Error: %v", mode, objectHash, objectPath, t.repo.FullName(), t.basePath, err) + return fmt.Errorf("Unable to add object to index at %s in temporary repo %s Error: %v", objectPath, t.repo.FullName(), err) } return nil } // WriteTree writes the current index as a tree to the object db and returns its hash func (t *TemporaryUploadRepository) WriteTree() (string, error) { - treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute, - t.basePath, - fmt.Sprintf("WriteTree (git write-tree): %s", t.basePath), - git.GitExecutable, "write-tree") + stdout, err := git.NewCommand("write-tree").RunInDir(t.basePath) if err != nil { - return "", fmt.Errorf("git write-tree: %s", stderr) + log.Error("Unable to write tree in temporary repo: %s(%s): Error: %v", t.repo.FullName(), t.basePath, err) + return "", fmt.Errorf("Unable to write-tree in temporary repo for: %s Error: %v", t.repo.FullName(), err) } - return strings.TrimSpace(treeHash), nil + return strings.TrimSpace(stdout), nil } // GetLastCommit gets the last commit ID SHA of the repo @@ -240,14 +178,12 @@ func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, erro if ref == "" { ref = "HEAD" } - treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute, - t.basePath, - fmt.Sprintf("GetLastCommit (git rev-parse %s): %s", ref, t.basePath), - git.GitExecutable, "rev-parse", ref) + stdout, err := git.NewCommand("rev-parse", ref).RunInDir(t.basePath) if err != nil { - return "", fmt.Errorf("git rev-parse %s: %s", ref, stderr) + log.Error("Unable to get last ref for %s in temporary repo: %s(%s): Error: %v", ref, t.repo.FullName(), t.basePath, err) + return "", fmt.Errorf("Unable to rev-parse %s in temporary repo for: %s Error: %v", ref, t.repo.FullName(), err) } - return strings.TrimSpace(treeHash), nil + return strings.TrimSpace(stdout), nil } // CommitTree creates a commit from a given tree for the user with provided message @@ -287,16 +223,15 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t } } - commitHash, stderr, err := process.GetManager().ExecDirEnvStdIn(5*time.Minute, - t.basePath, - fmt.Sprintf("commitTree (git commit-tree): %s", t.basePath), - env, - messageBytes, - git.GitExecutable, args...) - if err != nil { - return "", fmt.Errorf("git commit-tree: %s", stderr) + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + if err := git.NewCommand(args...).RunInDirTimeoutEnvFullPipeline(env, -1, t.basePath, stdout, stderr, messageBytes); err != nil { + log.Error("Unable to commit-tree in temporary repo: %s (%s) Error: %v\nStdout: %s\nStderr: %s", + t.repo.FullName(), t.basePath, err, stdout, stderr) + return "", fmt.Errorf("Unable to commit-tree in temporary repo: %s Error: %v\nStdout: %s\nStderr: %s", + t.repo.FullName(), err, stdout, stderr) } - return strings.TrimSpace(commitHash), nil + return strings.TrimSpace(stdout.String()), nil } // Push the provided commitHash to the repository branch by the provided user @@ -304,47 +239,48 @@ func (t *TemporaryUploadRepository) Push(doer *models.User, commitHash string, b // Because calls hooks we need to pass in the environment env := models.PushingEnvironment(doer, t.repo) - if _, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute, - t.basePath, - fmt.Sprintf("actuallyPush (git push): %s", t.basePath), - env, - git.GitExecutable, "push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)); err != nil { - return fmt.Errorf("git push: %s", stderr) + if _, err := git.NewCommand("push", t.repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)).RunInDirWithEnv(t.basePath, env); err != nil { + log.Error("Unable to push back to repo from temporary repo: %s (%s) Error: %v", + t.repo.FullName(), t.basePath, err) + return fmt.Errorf("Unable to push back to repo from temporary repo: %s (%s) Error: %v", + t.repo.FullName(), t.basePath, err) } return nil } // DiffIndex returns a Diff of the current index to the head -func (t *TemporaryUploadRepository) DiffIndex() (diff *gitdiff.Diff, err error) { - timeout := 5 * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - stdErr := new(bytes.Buffer) - - cmd := exec.CommandContext(ctx, git.GitExecutable, "diff-index", "--cached", "-p", "HEAD") - cmd.Dir = t.basePath - cmd.Stderr = stdErr - - stdout, err := cmd.StdoutPipe() +func (t *TemporaryUploadRepository) DiffIndex() (*gitdiff.Diff, error) { + stdoutReader, stdoutWriter, err := os.Pipe() if err != nil { - return nil, fmt.Errorf("StdoutPipe: %v stderr %s", err, stdErr.String()) + log.Error("Unable to open stdout pipe: %v", err) + return nil, fmt.Errorf("Unable to open stdout pipe: %v", err) } + defer func() { + _ = stdoutReader.Close() + _ = stdoutWriter.Close() + }() + stderr := new(bytes.Buffer) + var diff *gitdiff.Diff + var finalErr error - if err = cmd.Start(); err != nil { - return nil, fmt.Errorf("Start: %v stderr %s", err, stdErr.String()) - } - - pid := process.GetManager().Add(fmt.Sprintf("diffIndex [repo_path: %s]", t.repo.RepoPath()), cmd) - defer process.GetManager().Remove(pid) - - diff, err = gitdiff.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdout) - if err != nil { - return nil, fmt.Errorf("ParsePatch: %v", err) - } - - if err = cmd.Wait(); err != nil { - return nil, fmt.Errorf("Wait: %v", err) + if err := git.NewCommand("diff-index", "--cached", "-p", "HEAD"). + RunInDirTimeoutEnvFullPipelineFunc(nil, 30*time.Second, t.basePath, stdoutWriter, stderr, nil, func(ctx context.Context, cancel context.CancelFunc) { + _ = stdoutWriter.Close() + diff, finalErr = gitdiff.ParsePatch(setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, stdoutReader) + if finalErr != nil { + log.Error("ParsePatch: %v", finalErr) + cancel() + } + _ = stdoutReader.Close() + }); err != nil { + if finalErr != nil { + log.Error("Unable to ParsePatch in temporary repo %s (%s). Error: %v", t.repo.FullName(), t.basePath, finalErr) + return nil, finalErr + } + log.Error("Unable to run diff-index pipeline in temporary repo %s (%s). Error: %v\nStderr: %s", + t.repo.FullName(), t.basePath, err, stderr) + return nil, fmt.Errorf("Unable to run diff-index pipeline in temporary repo %s. Error: %v\nStderr: %s", + t.repo.FullName(), err, stderr) } return diff, nil @@ -358,12 +294,8 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str return nil, err } - stdOut := new(bytes.Buffer) - stdErr := new(bytes.Buffer) - - timeout := 5 * time.Minute - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) cmdArgs := []string{"check-attr", "-z", attribute} @@ -379,26 +311,14 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str } } - cmd := exec.CommandContext(ctx, git.GitExecutable, cmdArgs...) - desc := fmt.Sprintf("checkAttr: (git check-attr) %s %v", attribute, cmdArgs) - cmd.Dir = t.basePath - cmd.Stdout = stdOut - cmd.Stderr = stdErr - - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err()) + if err := git.NewCommand(cmdArgs...).RunInDirPipeline(t.basePath, stdout, stderr); err != nil { + log.Error("Unable to check-attr in temporary repo: %s (%s) Error: %v\nStdout: %s\nStderr: %s", + t.repo.FullName(), t.basePath, err, stdout, stderr) + return nil, fmt.Errorf("Unable to check-attr in temporary repo: %s Error: %v\nStdout: %s\nStderr: %s", + t.repo.FullName(), err, stdout, stderr) } - pid := process.GetManager().Add(desc, cmd) - err = cmd.Wait() - process.GetManager().Remove(pid) - - if err != nil { - err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr) - return nil, err - } - - fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) + fields := bytes.Split(stdout.Bytes(), []byte{'\000'}) if len(fields)%3 != 1 { return nil, fmt.Errorf("Wrong number of fields in return from check-attr") diff --git a/modules/repofiles/tree.go b/modules/repofiles/tree.go index 318a5d152d..cf0534563f 100644 --- a/modules/repofiles/tree.go +++ b/modules/repofiles/tree.go @@ -19,6 +19,7 @@ func GetTreeBySHA(repo *models.Repository, sha string, page, perPage int, recurs if err != nil { return nil, err } + defer gitRepo.Close() gitTree, err := gitRepo.GetTree(sha) if err != nil || gitTree == nil { return nil, models.ErrSHANotFound{ diff --git a/modules/repofiles/tree_test.go b/modules/repofiles/tree_test.go index ecff8b9071..e1bb812ec1 100644 --- a/modules/repofiles/tree_test.go +++ b/modules/repofiles/tree_test.go @@ -21,6 +21,8 @@ func TestGetTreeBySHA(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + sha := ctx.Repo.Repository.DefaultBranch page := 1 perPage := 10 diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index 8e057700ab..ef56609f4d 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -430,6 +430,7 @@ func PushUpdate(repo *models.Repository, branch string, opts models.PushUpdateOp if err != nil { return fmt.Errorf("OpenRepository: %v", err) } + defer gitRepo.Close() if err = repo.UpdateSize(); err != nil { log.Error("Failed to update size for repository: %v", err) @@ -504,5 +505,10 @@ func PushUpdate(repo *models.Repository, branch string, opts models.PushUpdateOp if opts.RefFullName == git.BranchPrefix+repo.DefaultBranch { models.UpdateRepoIndexer(repo) } + + if err = models.WatchIfAuto(opts.PusherID, repo.ID, true); err != nil { + log.Warn("Fail to perform auto watch on user %v for repo %v: %v", opts.PusherID, repo.ID, err) + } + return nil } diff --git a/modules/setting/service.go b/modules/setting/service.go index 93629100a2..6cbee8234d 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -44,6 +44,7 @@ var Service struct { NoReplyAddress string EnableUserHeatmap bool AutoWatchNewRepos bool + AutoWatchOnChanges bool DefaultOrgMemberVisible bool // OpenID settings @@ -85,6 +86,7 @@ func newService() { Service.NoReplyAddress = sec.Key("NO_REPLY_ADDRESS").MustString("noreply.example.org") Service.EnableUserHeatmap = sec.Key("ENABLE_USER_HEATMAP").MustBool(true) Service.AutoWatchNewRepos = sec.Key("AUTO_WATCH_NEW_REPOS").MustBool(true) + Service.AutoWatchOnChanges = sec.Key("AUTO_WATCH_ON_CHANGES").MustBool(false) Service.DefaultOrgVisibility = sec.Key("DEFAULT_ORG_VISIBILITY").In("public", structs.ExtractKeysFromMapString(structs.VisibilityModes)) Service.DefaultOrgVisibilityMode = structs.VisibilityModes[Service.DefaultOrgVisibility] Service.DefaultOrgMemberVisible = sec.Key("DEFAULT_ORG_MEMBER_VISIBLE").MustBool() diff --git a/modules/structs/repo.go b/modules/structs/repo.go index be6a3d4b43..ebfb0a0586 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -54,6 +54,7 @@ type Repository struct { Empty bool `json:"empty"` Private bool `json:"private"` Fork bool `json:"fork"` + Template bool `json:"template"` Parent *Repository `json:"parent"` Mirror bool `json:"mirror"` Size int `json:"size"` @@ -125,6 +126,8 @@ type EditRepoOption struct { // Note: you will get a 422 error if the organization restricts changing repository visibility to organization // owners and a non-owner tries to change the value of private. Private *bool `json:"private,omitempty"` + // either `true` to make this repository a template or `false` to make it a normal repository + Template *bool `json:"template,omitempty"` // either `true` to enable issues for this repository or `false` to disable them. HasIssues *bool `json:"has_issues,omitempty"` // set this structure to configure internal issue tracker (requires has_issues) diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go index 92df1c5762..cf9c5fbc54 100644 --- a/modules/test/context_tests.go +++ b/modules/test/context_tests.go @@ -55,6 +55,7 @@ func LoadRepo(t *testing.T, ctx *context.Context, repoID int64) { func LoadRepoCommit(t *testing.T, ctx *context.Context) { gitRepo, err := git.OpenRepository(ctx.Repo.Repository.RepoPath()) assert.NoError(t, err) + defer gitRepo.Close() branch, err := gitRepo.GetHEADBranch() assert.NoError(t, err) ctx.Repo.Commit, err = gitRepo.GetBranchCommit(branch.Name) diff --git a/modules/util/path.go b/modules/util/path.go index f79334209c..2b198eb6dc 100644 --- a/modules/util/path.go +++ b/modules/util/path.go @@ -4,7 +4,10 @@ package util -import "path/filepath" +import ( + "os" + "path/filepath" +) // EnsureAbsolutePath ensure that a path is absolute, making it // relative to absoluteBase if necessary @@ -14,3 +17,17 @@ func EnsureAbsolutePath(path string, absoluteBase string) string { } return filepath.Join(absoluteBase, path) } + +const notRegularFileMode os.FileMode = os.ModeDir | os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular + +// GetDirectorySize returns the dumb disk consumption for a given path +func GetDirectorySize(path string) (int64, error) { + var size int64 + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if info != nil && (info.Mode()¬RegularFileMode) == 0 { + size += info.Size() + } + return err + }) + return size, err +} diff --git a/modules/util/util.go b/modules/util/util.go index 4203b5eb51..6d02b5f52f 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -5,6 +5,7 @@ package util import ( + "bytes" "strings" ) @@ -63,3 +64,39 @@ func Min(a, b int) int { func IsEmptyString(s string) bool { return len(strings.TrimSpace(s)) == 0 } + +// NormalizeEOL will convert Windows (CRLF) and Mac (CR) EOLs to UNIX (LF) +func NormalizeEOL(input []byte) []byte { + var right, left, pos int + if right = bytes.IndexByte(input, '\r'); right == -1 { + return input + } + length := len(input) + tmp := make([]byte, length) + + // We know that left < length because otherwise right would be -1 from IndexByte. + copy(tmp[pos:pos+right], input[left:left+right]) + pos += right + tmp[pos] = '\n' + left += right + 1 + pos++ + + for left < length { + if input[left] == '\n' { + left++ + } + + right = bytes.IndexByte(input[left:], '\r') + if right == -1 { + copy(tmp[pos:], input[left:]) + pos += length - left + break + } + copy(tmp[pos:pos+right], input[left:left+right]) + pos += right + tmp[pos] = '\n' + left += right + 1 + pos++ + } + return tmp[:pos] +} diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 2475065059..04ab42f292 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -5,6 +5,7 @@ package util import ( + "strings" "testing" "code.gitea.io/gitea/modules/setting" @@ -94,3 +95,61 @@ func TestIsEmptyString(t *testing.T) { assert.Equal(t, v.expected, IsEmptyString(v.s)) } } + +func Test_NormalizeEOL(t *testing.T) { + data1 := []string{ + "", + "This text starts with empty lines", + "another", + "", + "", + "", + "Some other empty lines in the middle", + "more.", + "And more.", + "Ends with empty lines too.", + "", + "", + "", + } + + data2 := []string{ + "This text does not start with empty lines", + "another", + "", + "", + "", + "Some other empty lines in the middle", + "more.", + "And more.", + "Ends without EOLtoo.", + } + + buildEOLData := func(data []string, eol string) []byte { + return []byte(strings.Join(data, eol)) + } + + dos := buildEOLData(data1, "\r\n") + unix := buildEOLData(data1, "\n") + mac := buildEOLData(data1, "\r") + + assert.Equal(t, unix, NormalizeEOL(dos)) + assert.Equal(t, unix, NormalizeEOL(mac)) + assert.Equal(t, unix, NormalizeEOL(unix)) + + dos = buildEOLData(data2, "\r\n") + unix = buildEOLData(data2, "\n") + mac = buildEOLData(data2, "\r") + + assert.Equal(t, unix, NormalizeEOL(dos)) + assert.Equal(t, unix, NormalizeEOL(mac)) + assert.Equal(t, unix, NormalizeEOL(unix)) + + assert.Equal(t, []byte("one liner"), NormalizeEOL([]byte("one liner"))) + assert.Equal(t, []byte("\n"), NormalizeEOL([]byte("\n"))) + assert.Equal(t, []byte("\ntwo liner"), NormalizeEOL([]byte("\ntwo liner"))) + assert.Equal(t, []byte("two liner\n"), NormalizeEOL([]byte("two liner\n"))) + assert.Equal(t, []byte{}, NormalizeEOL([]byte{})) + + assert.Equal(t, []byte("mix\nand\nmatch\n."), NormalizeEOL([]byte("mix\r\nand\rmatch\n."))) +} diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 37adbbf9fc..02f3fa641c 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -218,6 +218,7 @@ forks=Разклонения + migrate_type=Тип мигриране migrate_type_helper=Това хранилище ще бъде огледало migrate_repo=Мигрирай хранилище diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index e95d786120..2e8cdee521 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -602,6 +602,7 @@ forks=Rozštěpení pick_reaction=Vyberte svoji reakci reactions_more=a %d dalších + archive.title=Tento repozitář je archivovaný. Můžete prohlížet soubory, klonovat, ale nemůžete nahrávat a vytvářet nové úkoly a požadavky na natažení. archive.issue.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat úkoly. archive.pull.nocomment=Tento repozitář je archivovaný. Nemůžete komentovat požadavky na natažení. @@ -1623,7 +1624,6 @@ users.auth_login_name=Přihlašovací jméno způsobu ověřování users.password_helper=Ponechte heslo prázdné, aby se nezměnilo. users.update_profile_success=Uživatelský účet byl aktualizován. users.edit_account=Upravit uživatelský účet -users.max_repo_creation=Maximální počet repozitářů users.max_repo_creation_desc=(Nastavte na -1 pro použití výchozího systémového limitu.) users.is_activated=Uživatelský účet je aktivován users.prohibit_login=Zakázat přihlášení diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 9897696fb4..4db25eaedb 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -68,6 +68,10 @@ pull_requests=Pull-Requests issues=Issues cancel=Abbrechen +add=Hinzufügen +add_all=Alle hinzufügen +remove=Löschen +remove_all=Alle entfernen write=Verfassen preview=Vorschau @@ -577,6 +581,7 @@ email_notifications.submit=E-Mail-Einstellungen festlegegen owner=Besitzer repo_name=Repository-Name repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern. +template_select=Vorlage auswählen visibility=Sichtbarkeit visibility_description=Nur der Besitzer oder Organisationsmitglieder mit entsprechender Berechtigung, werden in der Lage sein, es zu sehen. visibility_helper=In privates Repository umwandeln @@ -613,6 +618,7 @@ forks=Forks pick_reaction=Wähle eine Reaktion reactions_more=und %d weitere + archive.title=Dieses Repo ist archiviert. Du kannst Dateien sehen und es klonen, kannst aber nicht pushen oder Issues/Pull-Requests öffnen. archive.issue.nocomment=Dieses Repo ist archiviert. Du kannst Issues nicht kommentieren. archive.pull.nocomment=Dieses Repo ist archiviert. Du kannst Pull-Requests nicht kommentieren. @@ -1582,6 +1588,10 @@ teams.write_permission_desc=Dieses Team hat Schreibzugriff: Mit teams.admin_permission_desc=Dieses Team hat Adminzugriff: Mitglieder dieses Teams können Team-Repositories ansehen, auf sie pushen und Mitarbeiter hinzufügen. teams.repositories=Team-Repositories teams.search_repo_placeholder=Repository durchsuchen… +teams.remove_all_repos_title=Alle Team-Repositories entfernen +teams.remove_all_repos_desc=Dies entfernt alle Repositories von dem Team. +teams.add_all_repos_title=Alle Repositories hinzufügen +teams.add_all_repos_desc=Dadurch werden alle Repositories des Unternehmens dem Team hinzugefügt. teams.add_nonexistent_repo=Das Repository, das du hinzufügen möchten, existiert nicht. Bitte erstelle es zuerst. teams.add_duplicate_users=Dieser Benutzer ist bereits ein Teammitglied. teams.repos.none=Dieses Team hat Zugang zu keinem Repository. @@ -1685,7 +1695,6 @@ users.auth_login_name=Anmeldename zur Authentifizierung users.password_helper=Passwort leer lassen, um es nicht zu verändern. users.update_profile_success=Das Benutzerkonto wurde aktualisiert. users.edit_account=Benutzerkonto bearbeiten -users.max_repo_creation=Maximale Anzahl Repositories users.max_repo_creation_desc=(Gib -1 ein, um das globale Standardlimit zu verwenden.) users.is_activated=Account ist aktiviert users.prohibit_login=Anmelden deaktivieren diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4433c5bb2a..e3e0dba9fe 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -582,6 +582,11 @@ email_notifications.submit = Set Email Preference owner = Owner repo_name = Repository Name repo_name_helper = Good repository names use short, memorable and unique keywords. +repo_size = Repository Size +template = Template +template_select = Select a template. +template_helper = Make repository a template +template_description = Template repositories let users generate new repositories with the same directory structure, files, and optional settings. visibility = Visibility visibility_description = Only the owner or the organization members if they have rights, will be able to see it. visibility_helper = Make Repository Private @@ -591,6 +596,9 @@ clone_helper = Need help cloning? Visit peili migrate_repo=Siirrä repo diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 1f14967002..ba17418d66 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -610,6 +610,7 @@ forks=Bifurcations pick_reaction=Choisissez votre réaction reactions_more=et %d de plus + archive.title=Ce dépôt est archivé. Vous pouvez voir les fichiers et le cloner, mais vous ne pouvez pas pousser ni ouvrir de ticket/demande d'ajout. archive.issue.nocomment=Ce dépôt est archivé. Vous ne pouvez pas commenter de tickets. archive.pull.nocomment=Ce dépôt est archivé. Vous ne pouvez pas commenter de demande d'ajout. @@ -1646,7 +1647,6 @@ users.auth_login_name=Nom d'utilisateur pour l'authentification users.password_helper=Laissez le mot de passe vide pour le garder inchangé. users.update_profile_success=Le compte a bien été mis à jour. users.edit_account=Modifier un compte -users.max_repo_creation=Nombre maximal de dépôts users.max_repo_creation_desc=(Mettre à -1 pour utiliser la limite globale par défaut.) users.is_activated=Ce compte est activé users.prohibit_login=Désactiver la connexion diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 4992dbbef7..c2f5e6faea 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -284,6 +284,7 @@ pick_reaction=Válasszon reakciót reactions_more=és további %d + form.reach_limit_of_creation=Elérte a tárolók maximális számát (%d). form.name_reserved=A tárolónév ('%s') a rendszernek van fenntartva. diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index bbe4870cbb..17440dfb41 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -332,6 +332,7 @@ pick_reaction=Pilih reaksimu reactions_more=dan %d lainnya + form.reach_limit_of_creation=Anda telah mencapai batas dari repositori %d. form.name_reserved=Nama repositori '%s' dicadangkan. diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 00b6f3b378..643091d402 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -494,6 +494,7 @@ pick_reaction=Scegli la tua reazione reactions_more=e %d più + form.reach_limit_of_creation=Hai già raggiunto il tuo limite %d repository. form.name_reserved=Il nome repository '%s' è riservato. form.name_pattern_not_allowed=Il modello '%s' non è consentito come nome di un repository. @@ -1283,7 +1284,6 @@ users.auth_login_name=Nome utente per l'autenticazione users.password_helper=Lascia la password vuota per non modificarla. users.update_profile_success=L'account utente è stato aggiornato. users.edit_account=Modifica account utente -users.max_repo_creation=Numero massimo di repository users.max_repo_creation_desc=(Inserire -1 per utilizzare il limite predefinito globale.) users.is_activated=Account utente attivato users.prohibit_login=Disattiva login diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 1802108a50..1392d16344 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -581,6 +581,11 @@ email_notifications.submit=メール設定を保存 owner=オーナー repo_name=リポジトリ名 repo_name_helper=短く、覚えやすく、かつ他と重複しないキーワードを使用するのが良いリポジトリ名です。 +repo_size=リポジトリサイズ +template=テンプレート +template_select=テンプレートを選択してください。 +template_helper=テンプレートリポジトリにする +template_description=テンプレートリポジトリは、同じディレクトリ構成、同じファイル、同じオプション設定でユーザーが新しいリポジトリを作成できるようにするものです。 visibility=公開/非公開 visibility_description=オーナー、または権限を持つ組織のメンバーだけが、リポジトリを見ることができます。 visibility_helper=リポジトリをプライベートにする @@ -590,6 +595,7 @@ clone_helper=クローンに関してお困りであればогледало migrate_repo=Мигрирајте спремиште diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 7b737bded6..225ff89a93 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -511,6 +511,7 @@ pick_reaction=Välj din reaktion reactions_more=och %d flera + form.reach_limit_of_creation=Du har redan nått gränsen av %d repos. form.name_reserved=Utvecklingskatalogsnamnet '%s' är reserverat. form.name_pattern_not_allowed=Mönstret '%s' är otillåtet i ett utvecklingskatalogsnamn. @@ -1319,7 +1320,6 @@ users.auth_login_name=Användarnamn för inloggning users.password_helper=Lämna lösenordsfältet tomt att låta det förbli oförändrat. users.update_profile_success=Användarkontot har blivit uppdaterat. users.edit_account=Redigera användarkontot -users.max_repo_creation=Maximalt antal utvecklingskataloger users.max_repo_creation_desc=(Ange -1 för att använda global satt gräns.) users.is_activated=Användarkontot är aktiverat users.prohibit_login=Inaktivera inloggning diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index bfd42b40d8..d7439c9f37 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -67,6 +67,10 @@ pull_requests=Değişiklik İstekleri issues=Konular cancel=İptal +add=Ekle +add_all=Tümünü Ekle +remove=Kaldır +remove_all=Tümünü Kaldır write=Yaz preview=Önizleme @@ -542,6 +546,9 @@ email_notifications.submit=E-posta Tercihlerini Ayarla owner=Sahibi repo_name=Depo İsmi repo_name_helper=İyi bir depo ismi kısa, akılda kalıcı ve özgün anahtar kelimelerden oluşur. +template=Şablon +template_select=Bir şablon seçin. +template_helper=Depoyu şablon yap visibility=Görünürlük visibility_description=Yalnızca sahibi veya haklara sahip organizasyon üyeleri onu görebilecek. visibility_helper=Depoyu Gizli Yap @@ -551,6 +558,9 @@ clone_helper=Klonlama konusunda yardıma mı ihtiyacınız var? 0) { @@ -3294,7 +3341,7 @@ function initIssueList() { $('#new-dependency-drop-list') .dropdown({ apiSettings: { - url: issueSearchUrl, + url: issueSearchUrl, onResponse: function(response) { const filteredResponse = {'success': true, 'results': []}; const currIssueId = $('#new-dependency-drop-list').data('issue-id'); @@ -3305,7 +3352,7 @@ function initIssueList() { return; } filteredResponse.results.push({ - 'name' : '#' + issue.number + ' ' + htmlEncode(issue.title) + + 'name' : '#' + issue.number + ' ' + htmlEncode(issue.title) + '
' + htmlEncode(issue.repository.full_name) + '
', 'value' : issue.id }); diff --git a/public/js/semantic.dropdown.custom.js b/public/js/semantic.dropdown.custom.js index 1745869fbf..82e6efb2ec 100644 --- a/public/js/semantic.dropdown.custom.js +++ b/public/js/semantic.dropdown.custom.js @@ -997,7 +997,7 @@ $.fn.dropdown = function(parameters) { ; if(hasSelected && !module.is.multiple()) { module.debug('Forcing partial selection to selected item', $selectedItem); - $selectedItem[0].click(); + module.event.item.click.call($selectedItem, {}, true); return; } else { diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index cdec9e7fad..8d3b0123c5 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -8,8 +8,8 @@ package admin import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/user" ) diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index f35ad297b0..3a4750c6ba 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -10,10 +10,10 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/password" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/services/mailer" ) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index bd0bfb57b6..9139211ea6 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -641,7 +641,7 @@ func RegisterRoutes(m *macaron.Macaron) { }, reqRepoReader(models.UnitTypeCode)) m.Group("/tags", func() { m.Get("", repo.ListTags) - }, reqRepoReader(models.UnitTypeCode)) + }, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(true)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey) diff --git a/routers/api/v1/org/hook.go b/routers/api/v1/org/hook.go index 3f391e4b2b..c7b0bd5b6b 100644 --- a/routers/api/v1/org/hook.go +++ b/routers/api/v1/org/hook.go @@ -7,8 +7,8 @@ package org import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/utils" ) diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index 370536d22f..be47b6963f 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -9,9 +9,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/user" ) diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 8a1a478ba1..d698592361 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -8,8 +8,8 @@ package org import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/user" ) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index a22b60a2c6..b2b5fe6dad 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -10,9 +10,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/user" ) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 670123a858..9f6a2e6294 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -7,9 +7,9 @@ package repo import ( "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // GetBranch get a branch of a repository diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 132652e2a9..81d472dffb 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListCollaborators list a repository's collaborators diff --git a/routers/api/v1/repo/commits.go b/routers/api/v1/repo/commits.go index 0156aaaa05..163a06a95e 100644 --- a/routers/api/v1/repo/commits.go +++ b/routers/api/v1/repo/commits.go @@ -51,6 +51,7 @@ func GetSingleCommit(ctx *context.APIContext) { ctx.ServerError("OpenRepository", err) return } + defer gitRepo.Close() commit, err := gitRepo.GetCommit(ctx.Params(":sha")) if err != nil { ctx.NotFoundOrServerError("GetCommit", git.IsErrNotExist, err) @@ -113,6 +114,7 @@ func GetAllCommits(ctx *context.APIContext) { ctx.ServerError("OpenRepository", err) return } + defer gitRepo.Close() page := ctx.QueryInt("page") if page <= 0 { diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index f1a61bb0be..175235c5ef 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -95,6 +95,7 @@ func GetArchive(ctx *context.APIContext) { return } ctx.Repo.GitRepo = gitRepo + defer gitRepo.Close() repo.Download(ctx.Context) } diff --git a/routers/api/v1/repo/git_hook.go b/routers/api/v1/repo/git_hook.go index 80610356dd..46651ef614 100644 --- a/routers/api/v1/repo/git_hook.go +++ b/routers/api/v1/repo/git_hook.go @@ -6,9 +6,9 @@ package repo import ( "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListGitHooks list all Git hooks of a repository diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go index d7acc139f0..c2bcbb3603 100644 --- a/routers/api/v1/repo/git_ref.go +++ b/routers/api/v1/repo/git_ref.go @@ -76,6 +76,8 @@ func getGitRefs(ctx *context.APIContext, filter string) ([]*git.Reference, strin if err != nil { return nil, "OpenRepository", err } + defer gitRepo.Close() + if len(filter) > 0 { filter = "refs/" + filter } diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 18f1fba056..3666d79fa0 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -7,10 +7,10 @@ package repo import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/webhook" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/utils" ) diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go index 42bf024a5f..6b499d2fb1 100644 --- a/routers/api/v1/repo/key.go +++ b/routers/api/v1/repo/key.go @@ -9,9 +9,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // appendPrivateInformation appends the owner and key type information to api.PublicKey diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 6d86105a15..9abcaa0496 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -195,6 +195,7 @@ func CreatePullRequest(ctx *context.APIContext, form api.CreatePullRequestOption if ctx.Written() { return } + defer headGitRepo.Close() // Check if another PR exists with the same targets existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch) @@ -626,6 +627,18 @@ func MergePullRequest(ctx *context.APIContext, form auth.MergePullRequestForm) { if models.IsErrInvalidMergeStyle(err) { ctx.Status(405) return + } else if models.IsErrMergeConflicts(err) { + conflictError := err.(models.ErrMergeConflicts) + ctx.JSON(http.StatusConflict, conflictError) + } else if models.IsErrRebaseConflicts(err) { + conflictError := err.(models.ErrRebaseConflicts) + ctx.JSON(http.StatusConflict, conflictError) + } else if models.IsErrMergeUnrelatedHistories(err) { + conflictError := err.(models.ErrMergeUnrelatedHistories) + ctx.JSON(http.StatusConflict, conflictError) + } else if models.IsErrMergePushOutOfDate(err) { + ctx.Status(http.StatusConflict) + return } ctx.Error(500, "Merge", err) return @@ -710,6 +723,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) // user should have permission to read baseRepo's codes and pulls, NOT headRepo's permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User) if err != nil { + headGitRepo.Close() ctx.ServerError("GetUserRepoPermission", err) return nil, nil, nil, nil, "", "" } @@ -720,6 +734,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) baseRepo, permBase) } + headGitRepo.Close() ctx.NotFound("Can't read pulls or can't read UnitTypeCode") return nil, nil, nil, nil, "", "" } @@ -727,6 +742,7 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) // user should have permission to read headrepo's codes permHead, err := models.GetUserRepoPermission(headRepo, ctx.User) if err != nil { + headGitRepo.Close() ctx.ServerError("GetUserRepoPermission", err) return nil, nil, nil, nil, "", "" } @@ -737,18 +753,21 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) headRepo, permHead) } + headGitRepo.Close() ctx.NotFound("Can't read headRepo UnitTypeCode") return nil, nil, nil, nil, "", "" } // Check if head branch is valid. if !headGitRepo.IsBranchExist(headBranch) { + headGitRepo.Close() ctx.NotFound() return nil, nil, nil, nil, "", "" } compareInfo, err := headGitRepo.GetCompareInfo(models.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch) if err != nil { + headGitRepo.Close() ctx.Error(500, "GetCompareInfo", err) return nil, nil, nil, nil, "", "" } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7b752370d4..f7aa366b17 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations" "code.gitea.io/gitea/modules/notification" @@ -24,7 +25,6 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" - "code.gitea.io/gitea/routers/api/v1/convert" mirror_service "code.gitea.io/gitea/services/mirror" repo_service "code.gitea.io/gitea/services/repository" ) @@ -71,6 +71,11 @@ func Search(ctx *context.APIContext) { // description: search only for repos that the user with the given id owns or contributes to // type: integer // format: int64 + // - name: priority_owner_id + // in: query + // description: repo owner to prioritize in the results + // type: integer + // format: int64 // - name: starredBy // in: query // description: search only for repos that the user with the given id has starred @@ -80,6 +85,10 @@ func Search(ctx *context.APIContext) { // in: query // description: include private repositories this user has access to (defaults to true) // type: boolean + // - name: template + // in: query + // description: include template repositories this user has access to (defaults to true) + // type: boolean // - name: page // in: query // description: page number of results to return (1-based) @@ -116,17 +125,23 @@ func Search(ctx *context.APIContext) { opts := &models.SearchRepoOptions{ Keyword: strings.Trim(ctx.Query("q"), " "), OwnerID: ctx.QueryInt64("uid"), + PriorityOwnerID: ctx.QueryInt64("priority_owner_id"), Page: ctx.QueryInt("page"), PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")), TopicOnly: ctx.QueryBool("topic"), Collaborate: util.OptionalBoolNone, Private: ctx.IsSigned && (ctx.Query("private") == "" || ctx.QueryBool("private")), + Template: util.OptionalBoolNone, UserIsAdmin: ctx.IsUserSiteAdmin(), UserID: ctx.Data["SignedUserID"].(int64), StarredByID: ctx.QueryInt64("starredBy"), IncludeDescription: ctx.QueryBool("includeDesc"), } + if ctx.Query("template") != "" { + opts.Template = util.OptionalBoolOf(ctx.QueryBool("template")) + } + if ctx.QueryBool("exclusive") { opts.Collaborate = util.OptionalBoolFalse } @@ -678,6 +693,10 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err repo.IsPrivate = *opts.Private } + if opts.Template != nil { + repo.IsTemplate = *opts.Template + } + if err := models.UpdateRepository(repo, visibilityChanged); err != nil { ctx.Error(http.StatusInternalServerError, "UpdateRepository", err) return err diff --git a/routers/api/v1/repo/star.go b/routers/api/v1/repo/star.go index 2eb5daeced..8fe5b17c5f 100644 --- a/routers/api/v1/repo/star.go +++ b/routers/api/v1/repo/star.go @@ -6,8 +6,8 @@ package repo import ( "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListStargazers list a repository's stargazers diff --git a/routers/api/v1/repo/subscriber.go b/routers/api/v1/repo/subscriber.go index 79ad0e0221..0e576b4ff0 100644 --- a/routers/api/v1/repo/subscriber.go +++ b/routers/api/v1/repo/subscriber.go @@ -6,8 +6,8 @@ package repo import ( "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListSubscribers list a repo's subscribers (i.e. watchers) diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index a802048285..0a764113ab 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -8,8 +8,8 @@ import ( "net/http" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListTags list all the tags of a repository @@ -33,7 +33,7 @@ func ListTags(ctx *context.APIContext) { // responses: // "200": // "$ref": "#/responses/TagList" - tags, err := ctx.Repo.Repository.GetTags() + tags, err := ctx.Repo.GitRepo.GetTagInfos() if err != nil { ctx.Error(500, "GetTags", err) return diff --git a/routers/api/v1/repo/topic.go b/routers/api/v1/repo/topic.go index 6c3ac0020a..1656fd1b16 100644 --- a/routers/api/v1/repo/topic.go +++ b/routers/api/v1/repo/topic.go @@ -10,9 +10,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListTopics returns list of current topics for repo diff --git a/routers/api/v1/user/email.go b/routers/api/v1/user/email.go index 027f4e2763..8c0eb889ed 100644 --- a/routers/api/v1/user/email.go +++ b/routers/api/v1/user/email.go @@ -7,9 +7,9 @@ package user import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) // ListEmails list all of the authenticated user's email addresses diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 59df9665f4..ec512b9806 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -7,8 +7,8 @@ package user import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) func responseAPIUsers(ctx *context.APIContext, users []*models.User) { diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index fa85e47a82..82113caf0c 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -7,8 +7,8 @@ package user import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" ) func listGPGKeys(ctx *context.APIContext, uid int64) { diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 8425535b10..a812edfcc7 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -7,9 +7,9 @@ package user import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/repo" ) diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 0639494c0e..6f3fc4d32b 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -10,8 +10,8 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/routers/api/v1/convert" "github.com/unknwon/com" ) diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 6f72e99b71..f88b152003 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -11,9 +11,9 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/webhook" - "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/utils" "github.com/unknwon/com" diff --git a/routers/repo/compare.go b/routers/repo/compare.go index b9e14abfb8..e45f046435 100644 --- a/routers/repo/compare.go +++ b/routers/repo/compare.go @@ -157,6 +157,7 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, * // user should have permission to read baseRepo's codes and pulls, NOT headRepo's permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User) if err != nil { + headGitRepo.Close() ctx.ServerError("GetUserRepoPermission", err) return nil, nil, nil, nil, "", "" } @@ -167,6 +168,7 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, * baseRepo, permBase) } + headGitRepo.Close() ctx.NotFound("ParseCompareInfo", nil) return nil, nil, nil, nil, "", "" } @@ -174,6 +176,7 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, * // user should have permission to read headrepo's codes permHead, err := models.GetUserRepoPermission(headRepo, ctx.User) if err != nil { + headGitRepo.Close() ctx.ServerError("GetUserRepoPermission", err) return nil, nil, nil, nil, "", "" } @@ -184,6 +187,7 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, * headRepo, permHead) } + headGitRepo.Close() ctx.NotFound("ParseCompareInfo", nil) return nil, nil, nil, nil, "", "" } @@ -199,6 +203,7 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, * ctx.Data["HeadBranch"] = headBranch headIsCommit = true } else { + headGitRepo.Close() ctx.NotFound("IsRefExist", nil) return nil, nil, nil, nil, "", "" } @@ -219,12 +224,14 @@ func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, * baseRepo, permBase) } + headGitRepo.Close() ctx.NotFound("ParseCompareInfo", nil) return nil, nil, nil, nil, "", "" } compareInfo, err := headGitRepo.GetCompareInfo(models.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch) if err != nil { + headGitRepo.Close() ctx.ServerError("GetCompareInfo", err) return nil, nil, nil, nil, "", "" } @@ -356,6 +363,8 @@ func parseBaseRepoInfo(ctx *context.Context, repo *models.Repository) error { if err != nil { return err } + defer baseGitRepo.Close() + ctx.Data["BaseRepoBranches"], err = baseGitRepo.GetBranches() if err != nil { return err @@ -369,6 +378,8 @@ func CompareDiff(ctx *context.Context) { if ctx.Written() { return } + defer headGitRepo.Close() + if err := parseBaseRepoInfo(ctx, headRepo); err != nil { ctx.ServerError("parseBaseRepoInfo", err) return diff --git a/routers/repo/editor_test.go b/routers/repo/editor_test.go index ca00be74b7..ec7aee1e77 100644 --- a/routers/repo/editor_test.go +++ b/routers/repo/editor_test.go @@ -48,6 +48,8 @@ func TestGetUniquePatchBranchName(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + expectedBranchName := "user2-patch-1" branchName := GetUniquePatchBranchName(ctx) assert.Equal(t, expectedBranchName, branchName) @@ -61,9 +63,12 @@ func TestGetClosestParentWithFiles(t *testing.T) { test.LoadRepoCommit(t, ctx) test.LoadUser(t, ctx, 2) test.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + repo := ctx.Repo.Repository branch := repo.DefaultBranch gitRepo, _ := git.OpenRepository(repo.RepoPath()) + defer gitRepo.Close() commit, _ := gitRepo.GetBranchCommit(branch) expectedTreePath := "" diff --git a/routers/repo/pull.go b/routers/repo/pull.go index cb9c7c1164..67849f33e1 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -10,6 +10,7 @@ import ( "container/list" "crypto/subtle" "fmt" + "html" "io" "path" "strings" @@ -50,8 +51,8 @@ var ( } ) -func getForkRepository(ctx *context.Context) *models.Repository { - forkRepo, err := models.GetRepositoryByID(ctx.ParamsInt64(":repoid")) +func getRepository(ctx *context.Context, repoID int64) *models.Repository { + repo, err := models.GetRepositoryByID(repoID) if err != nil { if models.IsErrRepoNotExist(err) { ctx.NotFound("GetRepositoryByID", nil) @@ -61,25 +62,33 @@ func getForkRepository(ctx *context.Context) *models.Repository { return nil } - perm, err := models.GetUserRepoPermission(forkRepo, ctx.User) + perm, err := models.GetUserRepoPermission(repo, ctx.User) if err != nil { ctx.ServerError("GetUserRepoPermission", err) return nil } - if forkRepo.IsEmpty || !perm.CanRead(models.UnitTypeCode) { - if log.IsTrace() { - if forkRepo.IsEmpty { - log.Trace("Empty fork repository %-v", forkRepo) - } else { - log.Trace("Permission Denied: User %-v cannot read %-v of forkRepo %-v\n"+ - "User in forkRepo has Permissions: %-+v", - ctx.User, - models.UnitTypeCode, - ctx.Repo, - perm) - } - } + if !perm.CanRead(models.UnitTypeCode) { + log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+ + "User in repo has Permissions: %-+v", + ctx.User, + models.UnitTypeCode, + ctx.Repo, + perm) + ctx.NotFound("getRepository", nil) + return nil + } + return repo +} + +func getForkRepository(ctx *context.Context) *models.Repository { + forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid")) + if ctx.Written() { + return nil + } + + if forkRepo.IsEmpty { + log.Trace("Empty repository %-v", forkRepo) ctx.NotFound("getForkRepository", nil) return nil } @@ -89,7 +98,7 @@ func getForkRepository(ctx *context.Context) *models.Repository { ctx.Data["IsPrivate"] = forkRepo.IsPrivate canForkToUser := forkRepo.OwnerID != ctx.User.ID && !ctx.User.HasForkedRepo(forkRepo.ID) - if err = forkRepo.GetOwner(); err != nil { + if err := forkRepo.GetOwner(); err != nil { ctx.ServerError("GetOwner", err) return nil } @@ -108,6 +117,7 @@ func getForkRepository(ctx *context.Context) *models.Repository { } var traverseParentRepo = forkRepo + var err error for { if ctx.User.ID == traverseParentRepo.OwnerID { canForkToUser = false @@ -342,6 +352,7 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare ctx.ServerError("OpenRepository", err) return nil } + defer headGitRepo.Close() headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) @@ -524,6 +535,7 @@ func ViewPullFiles(ctx *context.Context) { ctx.ServerError("OpenRepository", err) return } + defer headGitRepo.Close() headCommitID, err := headGitRepo.GetBranchCommitID(pull.HeadBranch) if err != nil { @@ -660,10 +672,39 @@ func MergePullRequest(ctx *context.Context, form auth.MergePullRequestForm) { } if err = pull_service.Merge(pr, ctx.User, ctx.Repo.GitRepo, models.MergeStyle(form.Do), message); err != nil { + sanitize := func(x string) string { + runes := []rune(x) + + if len(runes) > 512 { + x = "..." + string(runes[len(runes)-512:]) + } + + return strings.Replace(html.EscapeString(x), "\n", "
", -1) + } if models.IsErrInvalidMergeStyle(err) { ctx.Flash.Error(ctx.Tr("repo.pulls.invalid_merge_option")) ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) return + } else if models.IsErrMergeConflicts(err) { + conflictError := err.(models.ErrMergeConflicts) + ctx.Flash.Error(ctx.Tr("repo.pulls.merge_conflict", sanitize(conflictError.StdErr), sanitize(conflictError.StdOut))) + ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) + return + } else if models.IsErrRebaseConflicts(err) { + conflictError := err.(models.ErrRebaseConflicts) + ctx.Flash.Error(ctx.Tr("repo.pulls.rebase_conflict", sanitize(conflictError.CommitSHA), sanitize(conflictError.StdErr), sanitize(conflictError.StdOut))) + ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) + return + } else if models.IsErrMergeUnrelatedHistories(err) { + log.Debug("MergeUnrelatedHistories error: %v", err) + ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories")) + ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) + return + } else if models.IsErrMergePushOutOfDate(err) { + log.Debug("MergePushOutOfDate error: %v", err) + ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date")) + ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(pr.Index)) + return } ctx.ServerError("Merge", err) return @@ -709,6 +750,7 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm) if ctx.Written() { return } + defer headGitRepo.Close() labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form, true) if ctx.Written() { @@ -874,12 +916,14 @@ func CleanUpPullRequest(ctx *context.Context) { ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err) return } + defer gitRepo.Close() gitBaseRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) if err != nil { ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err) return } + defer gitBaseRepo.Close() defer func() { ctx.JSON(200, map[string]interface{}{ @@ -1008,6 +1052,7 @@ func DownloadPullPatch(ctx *context.Context) { ctx.ServerError("OpenRepository", err) return } + defer headGitRepo.Close() patch, err := headGitRepo.GetFormatPatch(pr.MergeBase, pr.HeadBranch) if err != nil { diff --git a/routers/repo/release_test.go b/routers/repo/release_test.go index 524c1c7346..47d1a89b54 100644 --- a/routers/repo/release_test.go +++ b/routers/repo/release_test.go @@ -57,5 +57,6 @@ func TestNewReleasePost(t *testing.T) { Title: testCase.Form.Title, Note: testCase.Form.Content, }, models.Cond("is_draft=?", len(testCase.Form.Draft) > 0)) + ctx.Repo.GitRepo.Close() } } diff --git a/routers/repo/repo.go b/routers/repo/repo.go index cf1845a727..cb4e483333 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -129,6 +129,16 @@ func Create(ctx *context.Context) { } ctx.Data["ContextUser"] = ctxUser + ctx.Data["repo_template_name"] = ctx.Tr("repo.template_select") + templateID := ctx.QueryInt64("template_id") + if templateID > 0 { + templateRepo, err := models.GetRepositoryByID(templateID) + if err == nil && templateRepo.CheckUnitUser(ctxUser.ID, ctxUser.IsAdmin, models.UnitTypeCode) { + ctx.Data["repo_template"] = templateID + ctx.Data["repo_template_name"] = templateRepo.Name + } + } + ctx.HTML(200, tplCreate) } @@ -170,20 +180,53 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { return } - repo, err := repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ - Name: form.RepoName, - Description: form.Description, - Gitignores: form.Gitignores, - IssueLabels: form.IssueLabels, - License: form.License, - Readme: form.Readme, - IsPrivate: form.Private || setting.Repository.ForcePrivate, - AutoInit: form.AutoInit, - }) - if err == nil { - log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name) - return + var err error + if form.RepoTemplate > 0 { + opts := models.GenerateRepoOptions{ + Name: form.RepoName, + Description: form.Description, + Private: form.Private, + GitContent: form.GitContent, + Topics: form.Topics, + } + + if !opts.IsValid() { + ctx.RenderWithErr(ctx.Tr("repo.template.one_item"), tplCreate, form) + return + } + + templateRepo := getRepository(ctx, form.RepoTemplate) + if ctx.Written() { + return + } + + if !templateRepo.IsTemplate { + ctx.RenderWithErr(ctx.Tr("repo.template.invalid"), tplCreate, form) + return + } + + repo, err := repo_service.GenerateRepository(ctx.User, ctxUser, templateRepo, opts) + if err == nil { + log.Trace("Repository generated [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name) + return + } + } else { + repo, err := repo_service.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ + Name: form.RepoName, + Description: form.Description, + Gitignores: form.Gitignores, + IssueLabels: form.IssueLabels, + License: form.License, + Readme: form.Readme, + IsPrivate: form.Private || setting.Repository.ForcePrivate, + AutoInit: form.AutoInit, + }) + if err == nil { + log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) + ctx.Redirect(setting.AppSubURL + "/" + ctxUser.Name + "/" + repo.Name) + return + } } handleCreateError(ctx, ctxUser, err, "CreatePost", tplCreate, &form) diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 95cba3cbf2..fa215357d2 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -73,6 +73,11 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { // Check if repository name has been changed. if repo.LowerName != strings.ToLower(newRepoName) { isNameChanged = true + // Close the GitRepo if open + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + } if err := models.ChangeRepositoryName(ctx.Repo.Owner, repo.Name, newRepoName); err != nil { ctx.Data["Err_RepoName"] = true switch { @@ -101,6 +106,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { repo.LowerName = strings.ToLower(newRepoName) repo.Description = form.Description repo.Website = form.Website + repo.IsTemplate = form.Template // Visibility of forked repository is forced sync with base repository. if repo.IsFork { @@ -378,6 +384,11 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { } oldOwnerID := ctx.Repo.Owner.ID + // Close the GitRepo if open + if ctx.Repo.GitRepo != nil { + ctx.Repo.GitRepo.Close() + ctx.Repo.GitRepo = nil + } if err = models.TransferOwnership(ctx.User, newOwner, repo); err != nil { if models.IsErrRepoAlreadyExist(err) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) diff --git a/routers/repo/wiki.go b/routers/repo/wiki.go index 02fbe4a1dd..6cf1943658 100644 --- a/routers/repo/wiki.go +++ b/routers/repo/wiki.go @@ -146,6 +146,9 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { // Get page list. entries, err := commit.ListEntries() if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } ctx.ServerError("ListEntries", err) return nil, nil } @@ -159,6 +162,9 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if models.IsErrWikiInvalidFileName(err) { continue } + if wikiRepo != nil { + wikiRepo.Close() + } ctx.ServerError("WikiFilenameToName", err) return nil, nil } else if wikiName == "_Sidebar" || wikiName == "_Footer" { @@ -188,16 +194,25 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") } if entry == nil || ctx.Written() { + if wikiRepo != nil { + wikiRepo.Close() + } return nil, nil } sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar") if ctx.Written() { + if wikiRepo != nil { + wikiRepo.Close() + } return nil, nil } footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer") if ctx.Written() { + if wikiRepo != nil { + wikiRepo.Close() + } return nil, nil } @@ -218,6 +233,9 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { wikiRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } @@ -241,6 +259,9 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Redirect(ctx.Repo.RepoLink + "/wiki/_pages") } if entry == nil || ctx.Written() { + if wikiRepo != nil { + wikiRepo.Close() + } return nil, nil } @@ -263,6 +284,9 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRangeNoFollow("master", pageFilename, page) if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } ctx.ServerError("CommitsByFileAndRangeNoFollow", err) return nil, nil } @@ -279,13 +303,21 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } func renderEditPage(ctx *context.Context) { - _, commit, err := findWikiRepoCommit(ctx) + wikiRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return } + defer func() { + if wikiRepo != nil { + wikiRepo.Close() + } + }() // get requested pagename pageName := models.NormalizeWikiName(ctx.Params(":page")) @@ -327,8 +359,16 @@ func Wiki(ctx *context.Context) { wikiRepo, entry := renderViewPage(ctx) if ctx.Written() { + if wikiRepo != nil { + wikiRepo.Close() + } return } + defer func() { + if wikiRepo != nil { + wikiRepo.Close() + } + }() if entry == nil { ctx.Data["Title"] = ctx.Tr("repo.wiki") ctx.HTML(200, tplWikiStart) @@ -364,8 +404,16 @@ func WikiRevision(ctx *context.Context) { wikiRepo, entry := renderRevisionPage(ctx) if ctx.Written() { + if wikiRepo != nil { + wikiRepo.Close() + } return } + defer func() { + if wikiRepo != nil { + wikiRepo.Close() + } + }() if entry == nil { ctx.Data["Title"] = ctx.Tr("repo.wiki") ctx.HTML(200, tplWikiStart) @@ -397,11 +445,18 @@ func WikiPages(ctx *context.Context) { wikiRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } return } entries, err := commit.ListEntries() if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } + ctx.ServerError("ListEntries", err) return } @@ -412,6 +467,10 @@ func WikiPages(ctx *context.Context) { } c, err := wikiRepo.GetCommitByPath(entry.Name()) if err != nil { + if wikiRepo != nil { + wikiRepo.Close() + } + ctx.ServerError("GetCommit", err) return } @@ -420,6 +479,10 @@ func WikiPages(ctx *context.Context) { if models.IsErrWikiInvalidFileName(err) { continue } + if wikiRepo != nil { + wikiRepo.Close() + } + ctx.ServerError("WikiFilenameToName", err) return } @@ -431,6 +494,11 @@ func WikiPages(ctx *context.Context) { } ctx.Data["Pages"] = pages + defer func() { + if wikiRepo != nil { + wikiRepo.Close() + } + }() ctx.HTML(200, tplWikiPages) } diff --git a/routers/repo/wiki_test.go b/routers/repo/wiki_test.go index 4687d24f6b..44fcd02035 100644 --- a/routers/repo/wiki_test.go +++ b/routers/repo/wiki_test.go @@ -23,6 +23,7 @@ const message = "Wiki commit message for unit tests" func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.TreeEntry { wikiRepo, err := git.OpenRepository(repo.WikiPath()) assert.NoError(t, err) + defer wikiRepo.Close() commit, err := wikiRepo.GetBranchCommit("master") assert.NoError(t, err) entries, err := commit.ListEntries() diff --git a/routers/user/notification.go b/routers/user/notification.go index 8c23d76fa9..cd6617a233 100644 --- a/routers/user/notification.go +++ b/routers/user/notification.go @@ -68,6 +68,25 @@ func Notifications(c *context.Context) { return } + repos, err := notifications.LoadRepos() + if err != nil { + c.ServerError("LoadRepos", err) + return + } + if err := repos.LoadAttributes(); err != nil { + c.ServerError("LoadAttributes", err) + return + } + + if err := notifications.LoadIssues(); err != nil { + c.ServerError("LoadIssues", err) + return + } + if err := notifications.LoadComments(); err != nil { + c.ServerError("LoadComments", err) + return + } + total, err := models.GetNotificationCount(c.User, status) if err != nil { c.ServerError("ErrGetNotificationCount", err) diff --git a/services/comments/comments.go b/services/comments/comments.go index 1ae5e2743f..ba40bf582d 100644 --- a/services/comments/comments.go +++ b/services/comments/comments.go @@ -49,6 +49,7 @@ func CreateCodeComment(doer *models.User, repo *models.Repository, issue *models if err != nil { return nil, fmt.Errorf("OpenRepository: %v", err) } + defer gitRepo.Close() // FIXME validate treePath // Get latest commit referencing the commented line diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index c2c5675d9f..9c2aef5c97 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -678,6 +678,7 @@ func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID if err != nil { return nil, err } + defer gitRepo.Close() commit, err := gitRepo.GetCommit(afterCommitID) if err != nil { @@ -750,6 +751,7 @@ func GetRawDiffForFile(repoPath, startCommit, endCommit string, diffType RawDiff if err != nil { return fmt.Errorf("OpenRepository: %v", err) } + defer repo.Close() commit, err := repo.GetCommit(endCommit) if err != nil { diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 11430c2070..8c8131b5c2 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -197,8 +197,10 @@ func runSync(m *models.Mirror) ([]*mirrorSyncResult, bool) { return nil, false } if err = models.SyncReleasesWithTags(m.Repo, gitRepo); err != nil { + gitRepo.Close() log.Error("Failed to synchronize tags to releases for repository: %v", err) } + gitRepo.Close() if err := m.Repo.UpdateSize(); err != nil { log.Error("Failed to update size for mirror repository: %v", err) @@ -290,97 +292,103 @@ func Update() { func SyncMirrors() { // Start listening on new sync requests. for repoID := range mirrorQueue.Queue() { - log.Trace("SyncMirrors [repo_id: %v]", repoID) - mirrorQueue.Remove(repoID) + syncMirror(repoID) + } +} - m, err := models.GetMirrorByRepoID(com.StrTo(repoID).MustInt64()) +func syncMirror(repoID string) { + log.Trace("SyncMirrors [repo_id: %v]", repoID) + mirrorQueue.Remove(repoID) + + m, err := models.GetMirrorByRepoID(com.StrTo(repoID).MustInt64()) + if err != nil { + log.Error("GetMirrorByRepoID [%s]: %v", repoID, err) + return + + } + + results, ok := runSync(m) + if !ok { + return + } + + m.ScheduleNextUpdate() + if err = models.UpdateMirror(m); err != nil { + log.Error("UpdateMirror [%s]: %v", repoID, err) + return + } + + var gitRepo *git.Repository + if len(results) == 0 { + log.Trace("SyncMirrors [repo_id: %d]: no commits fetched", m.RepoID) + } else { + gitRepo, err = git.OpenRepository(m.Repo.RepoPath()) if err != nil { - log.Error("GetMirrorByRepoID [%s]: %v", repoID, err) + log.Error("OpenRepository [%d]: %v", m.RepoID, err) + return + } + defer gitRepo.Close() + } + + for _, result := range results { + // Discard GitHub pull requests, i.e. refs/pull/* + if strings.HasPrefix(result.refName, "refs/pull/") { continue } - results, ok := runSync(m) - if !ok { + // Create reference + if result.oldCommitID == gitShortEmptySha { + if err = SyncCreateAction(m.Repo, result.refName); err != nil { + log.Error("SyncCreateAction [repo_id: %d]: %v", m.RepoID, err) + } continue } - m.ScheduleNextUpdate() - if err = models.UpdateMirror(m); err != nil { - log.Error("UpdateMirror [%s]: %v", repoID, err) + // Delete reference + if result.newCommitID == gitShortEmptySha { + if err = SyncDeleteAction(m.Repo, result.refName); err != nil { + log.Error("SyncDeleteAction [repo_id: %d]: %v", m.RepoID, err) + } continue } - var gitRepo *git.Repository - if len(results) == 0 { - log.Trace("SyncMirrors [repo_id: %d]: no commits fetched", m.RepoID) - } else { - gitRepo, err = git.OpenRepository(m.Repo.RepoPath()) - if err != nil { - log.Error("OpenRepository [%d]: %v", m.RepoID, err) - continue - } - } - - for _, result := range results { - // Discard GitHub pull requests, i.e. refs/pull/* - if strings.HasPrefix(result.refName, "refs/pull/") { - continue - } - - // Create reference - if result.oldCommitID == gitShortEmptySha { - if err = SyncCreateAction(m.Repo, result.refName); err != nil { - log.Error("SyncCreateAction [repo_id: %d]: %v", m.RepoID, err) - } - continue - } - - // Delete reference - if result.newCommitID == gitShortEmptySha { - if err = SyncDeleteAction(m.Repo, result.refName); err != nil { - log.Error("SyncDeleteAction [repo_id: %d]: %v", m.RepoID, err) - } - continue - } - - // Push commits - oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID) - if err != nil { - log.Error("GetFullCommitID [%d]: %v", m.RepoID, err) - continue - } - newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID) - if err != nil { - log.Error("GetFullCommitID [%d]: %v", m.RepoID, err) - continue - } - commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID) - if err != nil { - log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err) - continue - } - if err = SyncPushAction(m.Repo, SyncPushActionOptions{ - RefName: result.refName, - OldCommitID: oldCommitID, - NewCommitID: newCommitID, - Commits: models.ListToPushCommits(commits), - }); err != nil { - log.Error("SyncPushAction [repo_id: %d]: %v", m.RepoID, err) - continue - } - } - - // Get latest commit date and update to current repository updated time - commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath()) + // Push commits + oldCommitID, err := git.GetFullCommitID(gitRepo.Path, result.oldCommitID) if err != nil { - log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err) + log.Error("GetFullCommitID [%d]: %v", m.RepoID, err) continue } + newCommitID, err := git.GetFullCommitID(gitRepo.Path, result.newCommitID) + if err != nil { + log.Error("GetFullCommitID [%d]: %v", m.RepoID, err) + continue + } + commits, err := gitRepo.CommitsBetweenIDs(newCommitID, oldCommitID) + if err != nil { + log.Error("CommitsBetweenIDs [repo_id: %d, new_commit_id: %s, old_commit_id: %s]: %v", m.RepoID, newCommitID, oldCommitID, err) + continue + } + if err = SyncPushAction(m.Repo, SyncPushActionOptions{ + RefName: result.refName, + OldCommitID: oldCommitID, + NewCommitID: newCommitID, + Commits: models.ListToPushCommits(commits), + }); err != nil { + log.Error("SyncPushAction [repo_id: %d]: %v", m.RepoID, err) + continue + } + } - if err = models.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil { - log.Error("Update repository 'updated_unix' [%d]: %v", m.RepoID, err) - continue - } + // Get latest commit date and update to current repository updated time + commitDate, err := git.GetLatestCommitTime(m.Repo.RepoPath()) + if err != nil { + log.Error("GetLatestCommitDate [%d]: %v", m.RepoID, err) + return + } + + if err = models.UpdateRepositoryUpdatedTime(m.RepoID, commitDate); err != nil { + log.Error("Update repository 'updated_unix' [%d]: %v", m.RepoID, err) + return } } diff --git a/services/mirror/mirror_test.go b/services/mirror/mirror_test.go index 9ad11b7265..37e8a7be57 100644 --- a/services/mirror/mirror_test.go +++ b/services/mirror/mirror_test.go @@ -51,6 +51,7 @@ func TestRelease_MirrorDelete(t *testing.T) { gitRepo, err := git.OpenRepository(repoPath) assert.NoError(t, err) + defer gitRepo.Close() findOptions := models.FindReleasesOptions{IncludeDrafts: true, IncludeTags: true} initCount, err := models.GetReleaseCountByRepoID(mirror.ID, findOptions) diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 2872db7bd2..ca00cdaad9 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -55,6 +55,7 @@ func IsPullCommitStatusPass(pr *models.PullRequest) (bool, error) { if err != nil { return false, errors.Wrap(err, "OpenRepository") } + defer headGitRepo.Close() if !headGitRepo.IsBranchExist(pr.HeadBranch) { return false, errors.New("Head branch does not exist, can not merge") diff --git a/services/pull/merge.go b/services/pull/merge.go index c6607910a2..5fbf550ad2 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -32,22 +32,27 @@ import ( func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) { binVersion, err := git.BinVersion() if err != nil { + log.Error("git.BinVersion: %v", err) return fmt.Errorf("Unable to get git version: %v", err) } if err = pr.GetHeadRepo(); err != nil { + log.Error("GetHeadRepo: %v", err) return fmt.Errorf("GetHeadRepo: %v", err) } else if err = pr.GetBaseRepo(); err != nil { + log.Error("GetBaseRepo: %v", err) return fmt.Errorf("GetBaseRepo: %v", err) } prUnit, err := pr.BaseRepo.GetUnit(models.UnitTypePullRequests) if err != nil { + log.Error("pr.BaseRepo.GetUnit(models.UnitTypePullRequests): %v", err) return err } prConfig := prUnit.PullRequestsConfig() if err := pr.CheckUserAllowedToMerge(doer); err != nil { + log.Error("CheckUserAllowedToMerge(%v): %v", doer, err) return fmt.Errorf("CheckUserAllowedToMerge: %v", err) } @@ -63,6 +68,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor // Clone base repo. tmpBasePath, err := models.CreateTemporaryPath("merge") if err != nil { + log.Error("CreateTemporaryPath: %v", err) return err } @@ -75,7 +81,8 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor headRepoPath := pr.HeadRepo.RepoPath() if err := git.InitRepository(tmpBasePath, false); err != nil { - return fmt.Errorf("git init: %v", err) + log.Error("git init tmpBasePath: %v", err) + return err } remoteRepoName := "head_repo" @@ -86,102 +93,141 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor p := filepath.Join(staging, ".git", "objects", "info", "alternates") f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) if err != nil { + log.Error("Could not create .git/objects/info/alternates file in %s: %v", staging, err) return err } defer f.Close() data := filepath.Join(cache, "objects") if _, err := fmt.Fprintln(f, data); err != nil { + log.Error("Could not write to .git/objects/info/alternates file in %s: %v", staging, err) return err } return nil } if err := addCacheRepo(tmpBasePath, baseGitRepo.Path); err != nil { - return fmt.Errorf("addCacheRepo [%s -> %s]: %v", headRepoPath, tmpBasePath, err) + log.Error("Unable to add base repository to temporary repo [%s -> %s]: %v", pr.BaseRepo.FullName(), tmpBasePath, err) + return fmt.Errorf("Unable to add base repository to temporary repo [%s -> tmpBasePath]: %v", pr.BaseRepo.FullName(), err) } - var errbuf strings.Builder - if err := git.NewCommand("remote", "add", "-t", pr.BaseBranch, "-m", pr.BaseBranch, "origin", baseGitRepo.Path).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git remote add [%s -> %s]: %s", baseGitRepo.Path, tmpBasePath, errbuf.String()) + var outbuf, errbuf strings.Builder + if err := git.NewCommand("remote", "add", "-t", pr.BaseBranch, "-m", pr.BaseBranch, "origin", baseGitRepo.Path).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to add base repository as origin [%s -> %s]: %v\n%s\n%s", pr.BaseRepo.FullName(), tmpBasePath, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("Unable to add base repository as origin [%s -> tmpBasePath]: %v\n%s\n%s", pr.BaseRepo.FullName(), err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() - if err := git.NewCommand("fetch", "origin", "--no-tags", pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + if err := git.NewCommand("fetch", "origin", "--no-tags", pr.BaseBranch+":"+baseBranch, pr.BaseBranch+":original_"+baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to fetch origin base branch [%s:%s -> base, original_base in %s]: %v:\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("Unable to fetch origin base branch [%s:%s -> base, original_base in tmpBasePath]: %v\n%s\n%s", pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() - if err := git.NewCommand("symbolic-ref", "HEAD", git.BranchPrefix+baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git symbolic-ref HEAD base [%s]: %s", tmpBasePath, errbuf.String()) + if err := git.NewCommand("symbolic-ref", "HEAD", git.BranchPrefix+baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to set HEAD as base branch [%s]: %v\n%s\n%s", tmpBasePath, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("Unable to set HEAD as base branch [tmpBasePath]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() if err := addCacheRepo(tmpBasePath, headRepoPath); err != nil { - return fmt.Errorf("addCacheRepo [%s -> %s]: %v", headRepoPath, tmpBasePath, err) + log.Error("Unable to add head repository to temporary repo [%s -> %s]: %v", pr.HeadRepo.FullName(), tmpBasePath, err) + return fmt.Errorf("Unable to head base repository to temporary repo [%s -> tmpBasePath]: %v", pr.HeadRepo.FullName(), err) } - if err := git.NewCommand("remote", "add", remoteRepoName, headRepoPath).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git remote add [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + if err := git.NewCommand("remote", "add", remoteRepoName, headRepoPath).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to add head repository as head_repo [%s -> %s]: %v\n%s\n%s", pr.HeadRepo.FullName(), tmpBasePath, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("Unable to add head repository as head_repo [%s -> tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() trackingBranch := "tracking" // Fetch head branch - if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git fetch [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() stagingBranch := "staging" // Enable sparse-checkout sparseCheckoutList, err := getDiffTree(tmpBasePath, baseBranch, trackingBranch) if err != nil { + log.Error("getDiffTree(%s, %s, %s): %v", tmpBasePath, baseBranch, trackingBranch, err) return fmt.Errorf("getDiffTree: %v", err) } infoPath := filepath.Join(tmpBasePath, ".git", "info") if err := os.MkdirAll(infoPath, 0700); err != nil { - return fmt.Errorf("creating directory failed [%s]: %v", infoPath, err) + log.Error("Unable to create .git/info in %s: %v", tmpBasePath, err) + return fmt.Errorf("Unable to create .git/info in tmpBasePath: %v", err) } + sparseCheckoutListPath := filepath.Join(infoPath, "sparse-checkout") if err := ioutil.WriteFile(sparseCheckoutListPath, []byte(sparseCheckoutList), 0600); err != nil { - return fmt.Errorf("Writing sparse-checkout file to %s: %v", sparseCheckoutListPath, err) + log.Error("Unable to write .git/info/sparse-checkout file in %s: %v", tmpBasePath, err) + return fmt.Errorf("Unable to write .git/info/sparse-checkout file in tmpBasePath: %v", err) } - gitConfigCommand := func() func() *git.Command { - binVersion, err := git.BinVersion() - if err != nil { - log.Fatal("Error retrieving git version: %v", err) + var gitConfigCommand func() *git.Command + if version.Compare(binVersion, "1.8.0", ">=") { + gitConfigCommand = func() *git.Command { + return git.NewCommand("config", "--local") } - - if version.Compare(binVersion, "1.8.0", ">=") { - return func() *git.Command { - return git.NewCommand("config", "--local") - } - } - return func() *git.Command { + } else { + gitConfigCommand = func() *git.Command { return git.NewCommand("config") } - }() + } // Switch off LFS process (set required, clean and smudge here also) - if err := gitConfigCommand().AddArguments("filter.lfs.process", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git config [filter.lfs.process -> <> ]: %v", errbuf.String()) - } - if err := gitConfigCommand().AddArguments("filter.lfs.required", "false").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git config [filter.lfs.required -> ]: %v", errbuf.String()) - } - if err := gitConfigCommand().AddArguments("filter.lfs.clean", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git config [filter.lfs.clean -> <> ]: %v", errbuf.String()) - } - if err := gitConfigCommand().AddArguments("filter.lfs.smudge", "").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %v", errbuf.String()) + if err := gitConfigCommand().AddArguments("filter.lfs.process", "").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git config [filter.lfs.process -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() - if err := gitConfigCommand().AddArguments("core.sparseCheckout", "true").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git config [core.sparsecheckout -> true]: %v", errbuf.String()) + if err := gitConfigCommand().AddArguments("filter.lfs.required", "false").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git config [filter.lfs.required -> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git config [filter.lfs.required -> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() + + if err := gitConfigCommand().AddArguments("filter.lfs.clean", "").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git config [filter.lfs.clean -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + } + outbuf.Reset() + errbuf.Reset() + + if err := gitConfigCommand().AddArguments("filter.lfs.smudge", "").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git config [filter.lfs.smudge -> <> ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + } + outbuf.Reset() + errbuf.Reset() + + if err := gitConfigCommand().AddArguments("core.sparseCheckout", "true").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git config [core.sparseCheckout -> true ]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git config [core.sparsecheckout -> true]: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + } + outbuf.Reset() + errbuf.Reset() // Read base branch index - if err := git.NewCommand("read-tree", "HEAD").RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git read-tree HEAD: %s", errbuf.String()) + if err := git.NewCommand("read-tree", "HEAD").RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git read-tree HEAD: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) + return fmt.Errorf("Unable to read base branch in to the index: %v\n%s\n%s", err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() // Determine if we should sign signArg := "" @@ -210,80 +256,102 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor // Merge commits. switch mergeStyle { case models.MergeStyleMerge: - if err := git.NewCommand("merge", "--no-ff", "--no-commit", trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + cmd := git.NewCommand("merge", "--no-ff", "--no-commit", trackingBranch) + if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { + log.Error("Unable to merge tracking into base: %v", err) + return err } - if signArg == "" { - if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - } - } else { - if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - } + if err := commitAndSignNoAuthor(pr, message, signArg, tmpBasePath, env); err != nil { + log.Error("Unable to make final commit: %v", err) + return err } case models.MergeStyleRebase: - // Checkout head branch - if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git checkout: %s", errbuf.String()) - } - // Rebase before merging - if err := git.NewCommand("rebase", "-q", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git rebase [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) - } - // Checkout base branch again - if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git checkout: %s", errbuf.String()) - } - // Merge fast forward - if err := git.NewCommand("merge", "--ff-only", "-q", stagingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git merge --ff-only [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) - } + fallthrough case models.MergeStyleRebaseMerge: // Checkout head branch - if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git checkout: %s", errbuf.String()) + if err := git.NewCommand("checkout", "-b", stagingBranch, trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() + // Rebase before merging - if err := git.NewCommand("rebase", "-q", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git rebase [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + if err := git.NewCommand("rebase", baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + // Rebase will leave a REBASE_HEAD file in .git if there is a conflict + if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "REBASE_HEAD")); statErr == nil { + // The original commit SHA1 that is failing will be in .git/rebase-apply/original-commit + commitShaBytes, readErr := ioutil.ReadFile(filepath.Join(tmpBasePath, ".git", "rebase-apply", "original-commit")) + if readErr != nil { + // Abandon this attempt to handle the error + log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + } + log.Debug("RebaseConflict at %s [%s:%s -> %s:%s]: %v\n%s\n%s", strings.TrimSpace(string(commitShaBytes)), pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return models.ErrRebaseConflicts{ + Style: mergeStyle, + CommitSHA: strings.TrimSpace(string(commitShaBytes)), + StdOut: outbuf.String(), + StdErr: errbuf.String(), + Err: err, + } + } + log.Error("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git rebase staging on to base [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() + // Checkout base branch again - if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git checkout: %s", errbuf.String()) - } - // Prepare merge with commit - if err := git.NewCommand("merge", "--no-ff", "--no-commit", "-q", stagingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git merge --no-ff [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + if err := git.NewCommand("checkout", baseBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git checkout base prior to merge post staging rebase [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } + outbuf.Reset() + errbuf.Reset() - // Set custom message and author and create merge commit - if signArg == "" { - if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - } + cmd := git.NewCommand("merge") + if mergeStyle == models.MergeStyleRebase { + cmd.AddArguments("--ff-only") } else { - if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + cmd.AddArguments("--no-ff", "--no-commit") + } + cmd.AddArguments(stagingBranch) + + // Prepare merge with commit + if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { + log.Error("Unable to merge staging into base: %v", err) + return err + } + if mergeStyle == models.MergeStyleRebaseMerge { + if err := commitAndSignNoAuthor(pr, message, signArg, tmpBasePath, env); err != nil { + log.Error("Unable to make final commit: %v", err) + return err } } - case models.MergeStyleSquash: // Merge with squash - if err := git.NewCommand("merge", "-q", "--squash", trackingBranch).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git merge --squash [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String()) + cmd := git.NewCommand("merge", "--squash", trackingBranch) + if err := runMergeCommand(pr, mergeStyle, cmd, tmpBasePath); err != nil { + log.Error("Unable to merge --squash tracking into base: %v", err) + return err } + sig := pr.Issue.Poster.NewGitSig() if signArg == "" { - if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } } else { - if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { - return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String()) + if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } } + outbuf.Reset() + errbuf.Reset() default: return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle} } @@ -329,9 +397,19 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor ) // Push back to upstream. - if err := git.NewCommand("push", "origin", baseBranch+":"+pr.BaseBranch).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil { + if err := git.NewCommand("push", "origin", baseBranch+":"+pr.BaseBranch).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { + if strings.Contains(errbuf.String(), "non-fast-forward") { + return models.ErrMergePushOutOfDate{ + Style: mergeStyle, + StdOut: outbuf.String(), + StdErr: errbuf.String(), + Err: err, + } + } return fmt.Errorf("git push: %s", errbuf.String()) } + outbuf.Reset() + errbuf.Reset() pr.MergedCommitID, err = baseGitRepo.GetBranchCommitID(pr.BaseBranch) if err != nil { @@ -364,6 +442,51 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor return nil } +func commitAndSignNoAuthor(pr *models.PullRequest, message, signArg, tmpBasePath string, env []string) error { + var outbuf, errbuf strings.Builder + if signArg == "" { + if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + } + } else { + if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { + log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + } + } + return nil +} + +func runMergeCommand(pr *models.PullRequest, mergeStyle models.MergeStyle, cmd *git.Command, tmpBasePath string) error { + var outbuf, errbuf strings.Builder + if err := cmd.RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { + // Merge will leave a MERGE_HEAD file in the .git folder if there is a conflict + if _, statErr := os.Stat(filepath.Join(tmpBasePath, ".git", "MERGE_HEAD")); statErr == nil { + // We have a merge conflict error + log.Debug("MergeConflict [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return models.ErrMergeConflicts{ + Style: mergeStyle, + StdOut: outbuf.String(), + StdErr: errbuf.String(), + Err: err, + } + } else if strings.Contains(errbuf.String(), "refusing to merge unrelated histories") { + log.Debug("MergeUnrelatedHistories [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return models.ErrMergeUnrelatedHistories{ + Style: mergeStyle, + StdOut: outbuf.String(), + StdErr: errbuf.String(), + Err: err, + } + } + log.Error("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + return fmt.Errorf("git merge [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) + } + + return nil +} + var escapedSymbols = regexp.MustCompile(`([*[?! \\])`) func getDiffTree(repoPath, baseBranch, headBranch string) (string, error) { diff --git a/services/pull/pull.go b/services/pull/pull.go index 4e981b2b26..7a9c2ef9ad 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -48,6 +48,7 @@ func checkForInvalidation(requests models.PullRequestList, repoID int64, doer *m if err != nil { log.Error("PullRequestList.InvalidateCodeComments: %v", err) } + gitRepo.Close() }() return nil } diff --git a/services/release/release_test.go b/services/release/release_test.go index d30dfee286..effab21251 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -27,6 +27,7 @@ func TestRelease_Create(t *testing.T) { gitRepo, err := git.OpenRepository(repoPath) assert.NoError(t, err) + defer gitRepo.Close() assert.NoError(t, CreateRelease(gitRepo, &models.Release{ RepoID: repo.ID, diff --git a/services/repository/repository.go b/services/repository/repository.go index 5135435c78..b1156b41d5 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -44,6 +44,21 @@ func ForkRepository(doer, u *models.User, oldRepo *models.Repository, name, desc return repo, nil } +// GenerateRepository generates a repository from a template +func GenerateRepository(doer, u *models.User, oldRepo *models.Repository, opts models.GenerateRepoOptions) (*models.Repository, error) { + repo, err := models.GenerateRepository(doer, u, oldRepo, opts) + if err != nil { + if repo != nil { + if errDelete := models.DeleteRepository(doer, u.ID, repo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } + return nil, err + } + + return repo, nil +} + // DeleteRepository deletes a repository for a user or organization. func DeleteRepository(doer *models.User, repo *models.Repository) error { if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index 1a53e3c893..f728a93631 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -55,68 +55,97 @@ -
- - -
- -
- -