code.gitea.io/gitea@v1.21.7/services/issue/template.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package issue
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"path"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/models/repo"
    14  	"code.gitea.io/gitea/modules/git"
    15  	"code.gitea.io/gitea/modules/issue/template"
    16  	"code.gitea.io/gitea/modules/log"
    17  	api "code.gitea.io/gitea/modules/structs"
    18  
    19  	"gopkg.in/yaml.v3"
    20  )
    21  
    22  // templateDirCandidates issue templates directory
    23  var templateDirCandidates = []string{
    24  	"ISSUE_TEMPLATE",
    25  	"issue_template",
    26  	".gitea/ISSUE_TEMPLATE",
    27  	".gitea/issue_template",
    28  	".github/ISSUE_TEMPLATE",
    29  	".github/issue_template",
    30  	".gitlab/ISSUE_TEMPLATE",
    31  	".gitlab/issue_template",
    32  }
    33  
    34  var templateConfigCandidates = []string{
    35  	".gitea/ISSUE_TEMPLATE/config",
    36  	".gitea/issue_template/config",
    37  	".github/ISSUE_TEMPLATE/config",
    38  	".github/issue_template/config",
    39  }
    40  
    41  func GetDefaultTemplateConfig() api.IssueConfig {
    42  	return api.IssueConfig{
    43  		BlankIssuesEnabled: true,
    44  		ContactLinks:       make([]api.IssueConfigContactLink, 0),
    45  	}
    46  }
    47  
    48  // GetTemplateConfig loads the given issue config file.
    49  // It never returns a nil config.
    50  func GetTemplateConfig(gitRepo *git.Repository, path string, commit *git.Commit) (api.IssueConfig, error) {
    51  	if gitRepo == nil {
    52  		return GetDefaultTemplateConfig(), nil
    53  	}
    54  
    55  	var err error
    56  
    57  	treeEntry, err := commit.GetTreeEntryByPath(path)
    58  	if err != nil {
    59  		return GetDefaultTemplateConfig(), err
    60  	}
    61  
    62  	reader, err := treeEntry.Blob().DataAsync()
    63  	if err != nil {
    64  		log.Debug("DataAsync: %v", err)
    65  		return GetDefaultTemplateConfig(), nil
    66  	}
    67  
    68  	defer reader.Close()
    69  
    70  	configContent, err := io.ReadAll(reader)
    71  	if err != nil {
    72  		return GetDefaultTemplateConfig(), err
    73  	}
    74  
    75  	issueConfig := GetDefaultTemplateConfig()
    76  	if err := yaml.Unmarshal(configContent, &issueConfig); err != nil {
    77  		return GetDefaultTemplateConfig(), err
    78  	}
    79  
    80  	for pos, link := range issueConfig.ContactLinks {
    81  		if link.Name == "" {
    82  			return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing name key", pos+1)
    83  		}
    84  
    85  		if link.URL == "" {
    86  			return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing url key", pos+1)
    87  		}
    88  
    89  		if link.About == "" {
    90  			return GetDefaultTemplateConfig(), fmt.Errorf("contact_link at position %d is missing about key", pos+1)
    91  		}
    92  
    93  		_, err = url.ParseRequestURI(link.URL)
    94  		if err != nil {
    95  			return GetDefaultTemplateConfig(), fmt.Errorf("%s is not a valid URL", link.URL)
    96  		}
    97  	}
    98  
    99  	return issueConfig, nil
   100  }
   101  
   102  // IsTemplateConfig returns if the given path is a issue config file.
   103  func IsTemplateConfig(path string) bool {
   104  	for _, configName := range templateConfigCandidates {
   105  		if path == configName+".yaml" || path == configName+".yml" {
   106  			return true
   107  		}
   108  	}
   109  	return false
   110  }
   111  
   112  // ParseTemplatesFromDefaultBranch parses the issue templates in the repo's default branch,
   113  // returns valid templates and the errors of invalid template files (the errors map is guaranteed to be non-nil).
   114  func ParseTemplatesFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (ret struct {
   115  	IssueTemplates []*api.IssueTemplate
   116  	TemplateErrors map[string]error
   117  },
   118  ) {
   119  	ret.TemplateErrors = map[string]error{}
   120  	if repo.IsEmpty {
   121  		return ret
   122  	}
   123  
   124  	commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
   125  	if err != nil {
   126  		return ret
   127  	}
   128  
   129  	for _, dirName := range templateDirCandidates {
   130  		tree, err := commit.SubTree(dirName)
   131  		if err != nil {
   132  			log.Debug("get sub tree of %s: %v", dirName, err)
   133  			continue
   134  		}
   135  		entries, err := tree.ListEntries()
   136  		if err != nil {
   137  			log.Debug("list entries in %s: %v", dirName, err)
   138  			return ret
   139  		}
   140  		for _, entry := range entries {
   141  			if !template.CouldBe(entry.Name()) {
   142  				continue
   143  			}
   144  			fullName := path.Join(dirName, entry.Name())
   145  			if it, err := template.UnmarshalFromEntry(entry, dirName); err != nil {
   146  				ret.TemplateErrors[fullName] = err
   147  			} else {
   148  				if !strings.HasPrefix(it.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
   149  					it.Ref = git.BranchPrefix + it.Ref
   150  				}
   151  				ret.IssueTemplates = append(ret.IssueTemplates, it)
   152  			}
   153  		}
   154  	}
   155  	return ret
   156  }
   157  
   158  // GetTemplateConfigFromDefaultBranch returns the issue config for this repo.
   159  // It never returns a nil config.
   160  func GetTemplateConfigFromDefaultBranch(repo *repo.Repository, gitRepo *git.Repository) (api.IssueConfig, error) {
   161  	if repo.IsEmpty {
   162  		return GetDefaultTemplateConfig(), nil
   163  	}
   164  
   165  	commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
   166  	if err != nil {
   167  		return GetDefaultTemplateConfig(), err
   168  	}
   169  
   170  	for _, configName := range templateConfigCandidates {
   171  		if _, err := commit.GetTreeEntryByPath(configName + ".yaml"); err == nil {
   172  			return GetTemplateConfig(gitRepo, configName+".yaml", commit)
   173  		}
   174  
   175  		if _, err := commit.GetTreeEntryByPath(configName + ".yml"); err == nil {
   176  			return GetTemplateConfig(gitRepo, configName+".yml", commit)
   177  		}
   178  	}
   179  
   180  	return GetDefaultTemplateConfig(), nil
   181  }
   182  
   183  func HasTemplatesOrContactLinks(repo *repo.Repository, gitRepo *git.Repository) bool {
   184  	ret := ParseTemplatesFromDefaultBranch(repo, gitRepo)
   185  	if len(ret.IssueTemplates) > 0 {
   186  		return true
   187  	}
   188  
   189  	issueConfig, _ := GetTemplateConfigFromDefaultBranch(repo, gitRepo)
   190  	return len(issueConfig.ContactLinks) > 0
   191  }