From daab2451676b1a3a5312af0e2a443e6017113702 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 10 Nov 2019 12:41:51 +0800 Subject: [PATCH 01/25] Move code.gitea.io/gitea/routers/api/v1/convert to code.gitea.io/gitea/modules/convert (#8892) * Move code.gitea.io/gitea/routers/api/v1/convert to code.gitea.io/gitea/modules/convert * fix fmt --- integrations/api_team_test.go | 2 +- integrations/api_team_user_test.go | 2 +- {routers/api/v1 => modules}/convert/convert.go | 0 {routers/api/v1 => modules}/convert/utils.go | 0 routers/api/v1/admin/org.go | 2 +- routers/api/v1/admin/user.go | 2 +- routers/api/v1/org/hook.go | 2 +- routers/api/v1/org/member.go | 2 +- routers/api/v1/org/org.go | 2 +- routers/api/v1/org/team.go | 2 +- routers/api/v1/repo/branch.go | 2 +- routers/api/v1/repo/collaborators.go | 2 +- routers/api/v1/repo/git_hook.go | 2 +- routers/api/v1/repo/hook.go | 2 +- routers/api/v1/repo/key.go | 2 +- routers/api/v1/repo/repo.go | 2 +- routers/api/v1/repo/star.go | 2 +- routers/api/v1/repo/subscriber.go | 2 +- routers/api/v1/repo/tag.go | 2 +- routers/api/v1/repo/topic.go | 2 +- routers/api/v1/user/email.go | 2 +- routers/api/v1/user/follower.go | 2 +- routers/api/v1/user/gpg_key.go | 2 +- routers/api/v1/user/key.go | 2 +- routers/api/v1/user/user.go | 2 +- routers/api/v1/utils/hook.go | 2 +- 26 files changed, 24 insertions(+), 24 deletions(-) rename {routers/api/v1 => modules}/convert/convert.go (100%) rename {routers/api/v1 => modules}/convert/utils.go (100%) 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/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/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/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/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/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/repo.go b/routers/api/v1/repo/repo.go index 7b752370d4..c907bba66b 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" ) 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..6cfdb461ee 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 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" From 31416a5f4e70d4972c351cde170b59d13fcbb77f Mon Sep 17 00:00:00 2001 From: 6543 <24977596+6543@users.noreply.github.com> Date: Sun, 10 Nov 2019 09:07:21 +0100 Subject: [PATCH 02/25] Fix API Bug (fail on empty assignees) (#8873) * keep sure if assigneeIDs == nil -> do nothing * fix #8872 * Revert "keep sure if assigneeIDs == nil -> do nothing" -> go handle it itself preaty well This reverts commit e72d94129c4666d5151f6131cb2f8c1de127d9d0. * clarity comparson Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * simplify * Update models/issue_assignees.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Update issue_assignees.go * simplify more * add --if oneAssignee != ""-- again * Update models/issue_assignees.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * CI.restart() * Update issue_assignees.go * add Test for GetUserIDsByNames * add Test for MakeIDsFromAPIAssigneesToAdd * fix test --- models/issue_assignees.go | 25 +++++++++++-------------- models/issue_assignees_test.go | 21 +++++++++++++++++++++ models/user_test.go | 13 +++++++++++++ 3 files changed, 45 insertions(+), 14 deletions(-) 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/user_test.go b/models/user_test.go index bcb955817c..f3952422af 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -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) +} From 8eeb2877d5803d0501815466d651a519b32bbd3a Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 10 Nov 2019 08:42:51 +0000 Subject: [PATCH 03/25] Adjust error reporting from merge failures and use LC_ALL=C for git (#8548) There are two major components to this PR: * This PR handles merge and rebase failures from merging a little more nicely with Flash errors rather a 500. * All git commands are run in the LC_ALL="C" environment to ensure that error messages are in English. This DefaultLocale is defined in a way that if necessary (due to platform weirdness) it can be overridden at build time using LDFLAGS="-X "code.gitea.io/gitea/modules/git.DefaultLocale=C"" with C changed for the locale as necessary. --- integrations/pull_merge_test.go | 137 ++++++++++++++ models/error.go | 73 ++++++++ modules/git/command.go | 11 +- options/locale/locale_en-US.ini | 4 + routers/api/v1/repo/pull.go | 12 ++ routers/repo/pull.go | 30 +++ services/pull/merge.go | 311 ++++++++++++++++++++++---------- 7 files changed, 483 insertions(+), 95 deletions(-) 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/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/modules/git/command.go b/modules/git/command.go index 347dcfe39f..2b5288aeab 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 @@ -77,7 +81,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 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4433c5bb2a..43b56f4ec7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1026,6 +1026,10 @@ pulls.rebase_merge_pull_request = Rebase and Merge pulls.rebase_merge_commit_pull_request = Rebase and Merge (--no-ff) pulls.squash_merge_pull_request = Squash and Merge pulls.invalid_merge_option = You cannot use this merge option for this pull request. +pulls.merge_conflict = Merge Failed: There was a conflict whilst merging: %[1]s
%[2]s
Hint: Try a different strategy +pulls.rebase_conflict = Merge Failed: There was a conflict whilst rebasing commit: %s[1]
%[1]s
%[2]s
Hint:Try a different strategy +pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy +pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.open_unmerged_pull_exists = `You cannot perform a reopen operation because there is a pending pull request (#%d) with identical properties.` pulls.status_checking = Some checks are pending pulls.status_checks_success = All checks were successful diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 6d86105a15..6af1ba1b04 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -626,6 +626,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 diff --git a/routers/repo/pull.go b/routers/repo/pull.go index cb9c7c1164..0eea5fcbe6 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" @@ -660,10 +661,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 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) { From 01a4a7cb14b3a48f9e8115d5bc93af7ae17f1275 Mon Sep 17 00:00:00 2001 From: guillep2k <18600385+guillep2k@users.noreply.github.com> Date: Sun, 10 Nov 2019 06:22:19 -0300 Subject: [PATCH 04/25] Auto-subscribe user to repository when they commit/tag to it (#7657) * Add support for AUTO_WATCH_ON_CHANGES and AUTO_WATCH_ON_CLONE * Update models/repo_watch.go Co-Authored-By: Lauris BH * Round up changes suggested by lafriks * Added changes suggested from automated tests * Updated deleteUser to take RepoWatchModeDont into account, corrected inverted DefaultWatchOnClone and DefaultWatchOnChanges behaviour, updated and added tests. * Reinsert import "github.com/Unknwon/com" on http.go * Add migration for new column `watch`.`mode` * Remove serv code * Remove WATCH_ON_CLONE; use hooks, add integrations * Renamed watch_test.go to repo_watch_test.go * Correct fmt * Add missing EOL * Correct name of test function * Reword cheat and ini descriptions * Add update to migration to ensure column value * Clarify comment Co-Authored-By: zeripath * Simplify if condition --- custom/conf/app.ini.sample | 3 + .../doc/advanced/config-cheat-sheet.en-us.md | 1 + integrations/repo_watch_test.go | 24 +++ models/consistency.go | 7 +- models/fixtures/repository.yml | 2 +- models/fixtures/watch.yml | 15 ++ models/migrations/migrations.go | 2 + models/migrations/v106.go | 26 ++++ models/repo.go | 4 +- models/repo_watch.go | 143 +++++++++++++++--- models/repo_watch_test.go | 90 ++++++++++- models/user.go | 3 +- modules/repofiles/update.go | 5 + modules/setting/service.go | 2 + 14 files changed, 297 insertions(+), 30 deletions(-) create mode 100644 integrations/repo_watch_test.go create mode 100644 models/migrations/v106.go 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/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/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/fixtures/repository.yml b/models/fixtures/repository.yml index cf7d24c6cd..32903723ec 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 - 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/migrations/migrations.go b/models/migrations/migrations.go index 5ed70dc4f5..71ffe2edb3 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -266,6 +266,8 @@ 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), } // 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/repo.go b/models/repo.go index 812460e92f..90918025fb 100644 --- a/models/repo.go +++ b/models/repo.go @@ -2410,8 +2410,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 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/modules/repofiles/update.go b/modules/repofiles/update.go index 8e057700ab..5479616c4b 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -504,5 +504,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() From 44ec9b933afe7a7c84e18ff525f377240d8dd4f0 Mon Sep 17 00:00:00 2001 From: Anthony Vanelverdinghe Date: Sun, 10 Nov 2019 17:49:39 +0100 Subject: [PATCH 05/25] Rephrase comment about RuntimeDirectory option (#8912) --- contrib/systemd/gitea.service | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From ee1d64ddd1456764de692fcdeb42db1398fcf97b Mon Sep 17 00:00:00 2001 From: zeripath Date: Sun, 10 Nov 2019 21:33:47 +0000 Subject: [PATCH 06/25] Stop using git count-objects and use raw directory size for repository (#8848) * Migrate from git count-objects to a raw directory size * As per @guillep2k ignore unusual files --- models/migrations/v28.go | 4 ++-- models/repo.go | 7 ++++--- modules/git/repo.go | 4 ++-- modules/util/path.go | 19 ++++++++++++++++++- 4 files changed, 26 insertions(+), 8 deletions(-) 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/repo.go b/models/repo.go index 90918025fb..d79a8fdf61 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" @@ -708,17 +709,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) } diff --git a/modules/git/repo.go b/modules/git/repo.go index 4c6690b913..80f6109772 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -304,8 +304,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/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 +} From 0e281384b56ff74ad795680a63cb574a27c2fdc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20H=C3=BCbner?= Date: Mon, 11 Nov 2019 02:33:28 +0100 Subject: [PATCH 07/25] Fix typo in doc (#8914) Fix typo in doc fail2ban-setup.md --- docs/content/doc/usage/fail2ban-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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.** From 273a24f22676b73a648fd2a5467e385ec41e84e2 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 11 Nov 2019 11:39:41 +0800 Subject: [PATCH 08/25] Move notifywatchers from models to notification (#8907) --- models/repo.go | 10 ---------- modules/notification/action/action.go | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/models/repo.go b/models/repo.go index d79a8fdf61..176182e67d 100644 --- a/models/repo.go +++ b/models/repo.go @@ -1470,16 +1470,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) 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) + } +} From 8d9e625f83bf05086cfb72087548d203aff96db3 Mon Sep 17 00:00:00 2001 From: David Svantesson Date: Mon, 11 Nov 2019 08:37:28 +0100 Subject: [PATCH 09/25] Only view branch or tag if it match refType requested. (#8899) * only view branch or tag if it match refName. * remove pointer in method --- modules/context/repo.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/modules/context/repo.go b/modules/context/repo.go index 8a9c9e4b8c..66f662ea0b 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -518,6 +518,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, "/") @@ -623,7 +639,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 +649,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 { From 74bb292fe3f4c02fc1dc5f32622c74d820cadd78 Mon Sep 17 00:00:00 2001 From: zeripath Date: Mon, 11 Nov 2019 11:46:28 +0000 Subject: [PATCH 10/25] Migrate temp_repo.go to use git.NewCommand (#8918) This PR migrates temp_repo.go to use git.NewCommand instead creating processes by itself - this fixes the problem underlying PR #8905. There are other places that run git outside of the controlled locale defined in #8548 but temp_repo.go is the only cause of failure of local testing in cases where English is not the default - implying that error messages from those other commands are not interpreted. Replaces #8905 --- modules/git/command.go | 11 ++ modules/repofiles/temp_repo.go | 241 +++++++++++---------------------- 2 files changed, 91 insertions(+), 161 deletions(-) diff --git a/modules/git/command.go b/modules/git/command.go index 2b5288aeab..7772abd2d5 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -67,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 } @@ -98,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/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index b07d2a8973..abc224c2c2 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" @@ -51,9 +49,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 +76,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 +87,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 +94,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 +105,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 +121,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 +177,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 +222,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 +238,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 +293,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 +310,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") From 74a6add4d90beb8133bcbf8ca6b43de35e0aa983 Mon Sep 17 00:00:00 2001 From: John Olheiser <42128690+jolheiser@users.noreply.github.com> Date: Mon, 11 Nov 2019 09:15:29 -0600 Subject: [PATCH 11/25] Template Repositories (#8768) * Start work on templates Signed-off-by: jolheiser * Continue work Signed-off-by: jolheiser * Fix IsTemplate vs IsGenerated Signed-off-by: jolheiser * Fix tabs vs spaces * Tabs vs Spaces * Add templates to API & start adding tests Signed-off-by: jolheiser * Fix integration tests Signed-off-by: jolheiser * Remove unused User Signed-off-by: jolheiser * Move template tests to existing repos Signed-off-by: jolheiser * Minor re-check updates and cleanup Signed-off-by: jolheiser * make fmt Signed-off-by: jolheiser * Test cleanup Signed-off-by: jolheiser * Fix optionalbool Signed-off-by: jolheiser * make fmt Signed-off-by: jolheiser * Test fixes and icon change Signed-off-by: jolheiser * Add new user and repo for tests Signed-off-by: jolheiser * Fix tests (finally) Signed-off-by: jolheiser * Update meta repo with env variables Signed-off-by: jolheiser * Move generation to create page Combine with repo create template Modify API search to prioritize owner for repo Signed-off-by: jolheiser * Fix tests and coverage Signed-off-by: jolheiser * Fix swagger and JS lint Signed-off-by: jolheiser * Fix API searching for own private repos Signed-off-by: jolheiser * Change wording Signed-off-by: jolheiser * Fix repo search test. User had a private repo that didn't show up Signed-off-by: jolheiser * Another search test fix Signed-off-by: jolheiser * Clarify git content Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Feedback updates Signed-off-by: jolheiser * Add topics WIP Signed-off-by: jolheiser * Finish adding topics Signed-off-by: jolheiser * Update locale Signed-off-by: jolheiser --- integrations/api_repo_test.go | 10 +- .../user27/template1.git/HEAD | 1 + .../user27/template1.git/config | 6 + .../user27/template1.git/description | 1 + .../template1.git/hooks/applypatch-msg.sample | 15 ++ .../template1.git/hooks/commit-msg.sample | 24 ++ .../hooks/fsmonitor-watchman.sample | 114 +++++++++ .../user27/template1.git/hooks/post-receive | 15 ++ .../template1.git/hooks/post-receive.d/gitea | 2 + .../template1.git/hooks/post-update.sample | 8 + .../template1.git/hooks/pre-applypatch.sample | 14 ++ .../template1.git/hooks/pre-commit.sample | 49 ++++ .../template1.git/hooks/pre-push.sample | 53 +++++ .../template1.git/hooks/pre-rebase.sample | 169 ++++++++++++++ .../user27/template1.git/hooks/pre-receive | 15 ++ .../template1.git/hooks/pre-receive.d/gitea | 2 + .../template1.git/hooks/pre-receive.sample | 24 ++ .../hooks/prepare-commit-msg.sample | 42 ++++ .../user27/template1.git/hooks/update | 14 ++ .../user27/template1.git/hooks/update.d/gitea | 2 + .../user27/template1.git/hooks/update.sample | 128 ++++++++++ .../user27/template1.git/info/exclude | 6 + .../user27/template1.git/info/refs | 1 + .../47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 | Bin 0 -> 84 bytes .../4d/31f3a12656368a8d9180f431d40d0fc408be2d | Bin 0 -> 29 bytes .../51/f84af231345367fd5d61ceb89efb3b6d757061 | Bin 0 -> 121 bytes .../79/3aa682b06ae032641abf70c5dfeade28c07c52 | Bin 0 -> 28 bytes .../aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 | Bin 0 -> 154 bytes .../dd/392e939ea4936b2459219c9c9a1f25547ccaeb | Bin 0 -> 53 bytes .../f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f | Bin 0 -> 54 bytes .../user27/template1.git/objects/info/packs | 1 + .../user27/template1.git/refs/heads/master | 1 + integrations/integration_test.go | 7 +- integrations/repo_generate_test.go | 67 ++++++ models/fixtures/repo_unit.yml | 14 ++ models/fixtures/repository.yml | 26 +++ models/fixtures/user.yml | 17 +- models/migrations/migrations.go | 2 + models/migrations/v107.go | 19 ++ models/repo.go | 219 ++++++++++++++++-- models/repo_list.go | 49 ++-- models/repo_list_test.go | 9 +- models/user_test.go | 2 +- modules/auth/repo_form.go | 5 + modules/context/repo.go | 27 +++ modules/structs/repo.go | 3 + options/locale/locale_en-US.ini | 14 ++ public/js/index.js | 51 +++- routers/api/v1/repo/repo.go | 19 ++ routers/repo/pull.go | 43 ++-- routers/repo/repo.go | 71 ++++-- routers/repo/setting.go | 1 + services/repository/repository.go | 15 ++ templates/repo/create.tmpl | 121 ++++++---- templates/repo/header.tmpl | 2 + templates/repo/home.tmpl | 11 + templates/repo/settings/options.tmpl | 7 + templates/swagger/v1_json.tmpl | 22 ++ 58 files changed, 1441 insertions(+), 119 deletions(-) create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/HEAD create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/config create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/description create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/applypatch-msg.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/commit-msg.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/fsmonitor-watchman.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/post-receive.d/gitea create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/post-update.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-applypatch.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-commit.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-push.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-rebase.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.d/gitea create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/pre-receive.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/prepare-commit-msg.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/update create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/update.d/gitea create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/hooks/update.sample create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/info/exclude create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/info/refs create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/47/34b1f84a367fa1b81c31aa4234a5bad11cafa3 create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/4d/31f3a12656368a8d9180f431d40d0fc408be2d create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/51/f84af231345367fd5d61ceb89efb3b6d757061 create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/79/3aa682b06ae032641abf70c5dfeade28c07c52 create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/aa/cbdfe9e1c4b47f60abe81849045fa4e96f1d75 create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/dd/392e939ea4936b2459219c9c9a1f25547ccaeb create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/f2/8eeca3df7614fd4f10c1030f13feb418ef3c6f create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/objects/info/packs create mode 100644 integrations/gitea-repositories-meta/user27/template1.git/refs/heads/master create mode 100644 integrations/repo_generate_test.go create mode 100644 models/migrations/v107.go 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/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 0000000000000000000000000000000000000000..b6f121a4bb5d3bef8b829a9b56eb50fd3dcf8fce GIT binary patch literal 84 zcmV-a0IUCa0V^p=O;s?nU@$Z=Ff%bx2y%6F@paY9O=0jg{Jc;t%&e<-V#621E4=(i qIQHq97yyAnNosKk!(B_g$@7*>&Q^(3oHJ*ZylP0zsn-Cegc}#|fhG+A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d2f4c1d04ed06ac5c7caf9aca1f44840f06a5f81 GIT binary patch literal 29 kcmblhEoiK()F0GTUJQ8B?D`S@e0H0(Eg8%>k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..74419f4b47cf77881c7d2b8f6716c5162c0c1584 GIT binary patch literal 154 zcmV;L0A>Gp0hNwB4#F@H1*v@scc}lL zw%||FO3^N$i%9V=y!@Ff%bxC`m0Y(JQGaVW_lP*0dq(fl-Ro{(__TU)|F< LP!j|IN#_x*?Qaz* literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0699bff833bc1e0a42bfdff36ad7fef2f93505c6 GIT binary patch literal 54 zcmV-60LlM&0V^p=O;s>9XD~D{Ff%bx2y%6F@paY9O=0jg{Jc;t%&e<-V#621E4=(i MIQHoR04~iA?=E#0MF0Q* literal 0 HcmV?d00001 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/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/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 32903723ec..4cec6471e9 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -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/migrations/migrations.go b/models/migrations/migrations.go index 71ffe2edb3..2e289b6f73 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -268,6 +268,8 @@ var migrations = []Migration{ 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), } // Migrate database to current version 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/repo.go b/models/repo.go index 176182e67d..ccecfe2fdf 100644 --- a/models/repo.go +++ b/models/repo.go @@ -179,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"` @@ -351,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), @@ -663,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) } @@ -1220,6 +1245,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) @@ -1323,8 +1362,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) @@ -1336,6 +1422,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())) @@ -1376,6 +1470,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"} @@ -2523,6 +2648,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) @@ -2593,27 +2740,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_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/user_test.go b/models/user_test.go index f3952422af..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}) 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/repo.go b/modules/context/repo.go index 66f662ea0b..bd3456773f 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 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/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 43b56f4ec7..dc9cc3d6b9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -582,6 +582,10 @@ email_notifications.submit = Set Email Preference owner = Owner repo_name = Repository Name repo_name_helper = Good repository names use short, memorable and unique keywords. +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 +595,9 @@ clone_helper = Need help cloning? Visit 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/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index c907bba66b..f7aa366b17 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -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/repo/pull.go b/routers/repo/pull.go index 0eea5fcbe6..8269717e57 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -51,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) @@ -62,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 } @@ -90,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 } @@ -109,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 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..f699c1d685 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -101,6 +101,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 { 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 @@ -
- - -
- -
- -