summaryrefslogtreecommitdiffstats
path: root/models/repo.go
diff options
context:
space:
mode:
authorJohn Olheiser <42128690+jolheiser@users.noreply.github.com>2019-11-11 09:15:29 -0600
committerLunny Xiao <xiaolunwen@gmail.com>2019-11-11 23:15:29 +0800
commit74a6add4d90beb8133bcbf8ca6b43de35e0aa983 (patch)
tree868e452d41d71094c5b2cccce67f4211fd14e77b /models/repo.go
parent74bb292fe3f4c02fc1dc5f32622c74d820cadd78 (diff)
downloadgitea-74a6add4d90beb8133bcbf8ca6b43de35e0aa983.tar.gz
gitea-74a6add4d90beb8133bcbf8ca6b43de35e0aa983.zip
Template Repositories (#8768)
* Start work on templates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Continue work Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix IsTemplate vs IsGenerated Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix tabs vs spaces * Tabs vs Spaces * Add templates to API & start adding tests Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix integration tests Signed-off-by: jolheiser <john.olheiser@gmail.com> * Remove unused User Signed-off-by: jolheiser <john.olheiser@gmail.com> * Move template tests to existing repos Signed-off-by: jolheiser <john.olheiser@gmail.com> * Minor re-check updates and cleanup Signed-off-by: jolheiser <john.olheiser@gmail.com> * make fmt Signed-off-by: jolheiser <john.olheiser@gmail.com> * Test cleanup Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix optionalbool Signed-off-by: jolheiser <john.olheiser@gmail.com> * make fmt Signed-off-by: jolheiser <john.olheiser@gmail.com> * Test fixes and icon change Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add new user and repo for tests Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix tests (finally) Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update meta repo with env variables Signed-off-by: jolheiser <john.olheiser@gmail.com> * Move generation to create page Combine with repo create template Modify API search to prioritize owner for repo Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix tests and coverage Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix swagger and JS lint Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix API searching for own private repos Signed-off-by: jolheiser <john.olheiser@gmail.com> * Change wording Signed-off-by: jolheiser <john.olheiser@gmail.com> * Fix repo search test. User had a private repo that didn't show up Signed-off-by: jolheiser <john.olheiser@gmail.com> * Another search test fix Signed-off-by: jolheiser <john.olheiser@gmail.com> * Clarify git content Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * Feedback updates Signed-off-by: jolheiser <john.olheiser@gmail.com> * Add topics WIP Signed-off-by: jolheiser <john.olheiser@gmail.com> * Finish adding topics Signed-off-by: jolheiser <john.olheiser@gmail.com> * Update locale Signed-off-by: jolheiser <john.olheiser@gmail.com>
Diffstat (limited to 'models/repo.go')
-rw-r--r--models/repo.go219
1 files changed, 206 insertions, 13 deletions
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)
+ }
+ }
+
+ if opts.Topics {
+ for _, topic := range templateRepo.Topics {
+ if _, err = addTopicByNameToRepo(sess, repo.ID, topic); err != nil {
+ return repo, err
+ }
+ }
}
- return repo, sess2.Commit()
+ return repo, sess.Commit()
}
// GetForks returns all the forks of the repository