Skip to content

Commit 90ab305

Browse files
davidsvantessonlunny
authored andcommitted
Api: advanced settings for repository (external wiki, issue tracker etc.) (#7756)
* Add API for Repo Advanced Settings of wiki and issue tracker Signed-off-by: David Svantesson <[email protected]> * Add some integration tests for tracker and wiki settings through API * Should return StatusUnprocessableEntity in case of invalid API values. * Add tests for invalid URLs for external tracker and wiki. * Do not set inital values if they are default of type * Make issue tracker and wiki units separate structures in Repository API structure. Signed-off-by: David Svantesson <[email protected]> * Fix comment of structures Signed-off-by: David Svantesson <[email protected]> * Rewrite API to use struct for setting tracker and wiki settings. * LetOnlyContributorsTrackTime -> AllowOnlyContributorsToTrackTime
1 parent f889967 commit 90ab305

File tree

5 files changed

+327
-41
lines changed

5 files changed

+327
-41
lines changed

integrations/api_repo_edit_test.go

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,35 @@ func getRepoEditOptionFromRepo(repo *models.Repository) *api.EditRepoOption {
2323
website := repo.Website
2424
private := repo.IsPrivate
2525
hasIssues := false
26-
if _, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
26+
var internalTracker *api.InternalTracker
27+
var externalTracker *api.ExternalTracker
28+
if unit, err := repo.GetUnit(models.UnitTypeIssues); err == nil {
29+
config := unit.IssuesConfig()
2730
hasIssues = true
31+
internalTracker = &api.InternalTracker{
32+
EnableTimeTracker: config.EnableTimetracker,
33+
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
34+
EnableIssueDependencies: config.EnableDependencies,
35+
}
36+
} else if unit, err := repo.GetUnit(models.UnitTypeExternalTracker); err == nil {
37+
config := unit.ExternalTrackerConfig()
38+
hasIssues = true
39+
externalTracker = &api.ExternalTracker{
40+
ExternalTrackerURL: config.ExternalTrackerURL,
41+
ExternalTrackerFormat: config.ExternalTrackerFormat,
42+
ExternalTrackerStyle: config.ExternalTrackerStyle,
43+
}
2844
}
2945
hasWiki := false
46+
var externalWiki *api.ExternalWiki
3047
if _, err := repo.GetUnit(models.UnitTypeWiki); err == nil {
3148
hasWiki = true
49+
} else if unit, err := repo.GetUnit(models.UnitTypeExternalWiki); err == nil {
50+
hasWiki = true
51+
config := unit.ExternalWikiConfig()
52+
externalWiki = &api.ExternalWiki{
53+
ExternalWikiURL: config.ExternalWikiURL,
54+
}
3255
}
3356
defaultBranch := repo.DefaultBranch
3457
hasPullRequests := false
@@ -53,7 +76,10 @@ func getRepoEditOptionFromRepo(repo *models.Repository) *api.EditRepoOption {
5376
Website: &website,
5477
Private: &private,
5578
HasIssues: &hasIssues,
79+
ExternalTracker: externalTracker,
80+
InternalTracker: internalTracker,
5681
HasWiki: &hasWiki,
82+
ExternalWiki: externalWiki,
5783
DefaultBranch: &defaultBranch,
5884
HasPullRequests: &hasPullRequests,
5985
IgnoreWhitespaceConflicts: &ignoreWhitespaceConflicts,
@@ -143,6 +169,84 @@ func TestAPIRepoEdit(t *testing.T) {
143169
assert.Equal(t, *repoEditOption.Archived, *repo1editedOption.Archived)
144170
assert.Equal(t, *repoEditOption.Private, *repo1editedOption.Private)
145171
assert.Equal(t, *repoEditOption.HasWiki, *repo1editedOption.HasWiki)
172+
173+
//Test editing repo1 to use internal issue and wiki (default)
174+
*repoEditOption.HasIssues = true
175+
repoEditOption.ExternalTracker = nil
176+
repoEditOption.InternalTracker = &api.InternalTracker{
177+
EnableTimeTracker: false,
178+
AllowOnlyContributorsToTrackTime: false,
179+
EnableIssueDependencies: false,
180+
}
181+
*repoEditOption.HasWiki = true
182+
repoEditOption.ExternalWiki = nil
183+
url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
184+
req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
185+
resp = session.MakeRequest(t, req, http.StatusOK)
186+
DecodeJSON(t, resp, &repo)
187+
assert.NotNil(t, repo)
188+
// check repo1 was written to database
189+
repo1edited = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
190+
repo1editedOption = getRepoEditOptionFromRepo(repo1edited)
191+
assert.Equal(t, *repo1editedOption.HasIssues, true)
192+
assert.Nil(t, repo1editedOption.ExternalTracker)
193+
assert.Equal(t, *repo1editedOption.InternalTracker, *repoEditOption.InternalTracker)
194+
assert.Equal(t, *repo1editedOption.HasWiki, true)
195+
assert.Nil(t, repo1editedOption.ExternalWiki)
196+
197+
//Test editing repo1 to use external issue and wiki
198+
repoEditOption.ExternalTracker = &api.ExternalTracker{
199+
ExternalTrackerURL: "http://www.somewebsite.com",
200+
ExternalTrackerFormat: "http://www.somewebsite.com/{user}/{repo}?issue={index}",
201+
ExternalTrackerStyle: "alphanumeric",
202+
}
203+
repoEditOption.ExternalWiki = &api.ExternalWiki{
204+
ExternalWikiURL: "http://www.somewebsite.com",
205+
}
206+
req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
207+
resp = session.MakeRequest(t, req, http.StatusOK)
208+
DecodeJSON(t, resp, &repo)
209+
assert.NotNil(t, repo)
210+
// check repo1 was written to database
211+
repo1edited = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
212+
repo1editedOption = getRepoEditOptionFromRepo(repo1edited)
213+
assert.Equal(t, *repo1editedOption.HasIssues, true)
214+
assert.Equal(t, *repo1editedOption.ExternalTracker, *repoEditOption.ExternalTracker)
215+
assert.Equal(t, *repo1editedOption.HasWiki, true)
216+
assert.Equal(t, *repo1editedOption.ExternalWiki, *repoEditOption.ExternalWiki)
217+
218+
// Do some tests with invalid URL for external tracker and wiki
219+
repoEditOption.ExternalTracker.ExternalTrackerURL = "htp://www.somewebsite.com"
220+
req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
221+
resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
222+
repoEditOption.ExternalTracker.ExternalTrackerURL = "http://www.somewebsite.com"
223+
repoEditOption.ExternalTracker.ExternalTrackerFormat = "http://www.somewebsite.com/{user/{repo}?issue={index}"
224+
req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
225+
resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
226+
repoEditOption.ExternalTracker.ExternalTrackerFormat = "http://www.somewebsite.com/{user}/{repo}?issue={index}"
227+
repoEditOption.ExternalWiki.ExternalWikiURL = "htp://www.somewebsite.com"
228+
req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
229+
resp = session.MakeRequest(t, req, http.StatusUnprocessableEntity)
230+
231+
//Test small repo change through API with issue and wiki option not set; They shall not be touched.
232+
*repoEditOption.Description = "small change"
233+
repoEditOption.HasIssues = nil
234+
repoEditOption.ExternalTracker = nil
235+
repoEditOption.HasWiki = nil
236+
repoEditOption.ExternalWiki = nil
237+
req = NewRequestWithJSON(t, "PATCH", url, &repoEditOption)
238+
resp = session.MakeRequest(t, req, http.StatusOK)
239+
DecodeJSON(t, resp, &repo)
240+
assert.NotNil(t, repo)
241+
// check repo1 was written to database
242+
repo1edited = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
243+
repo1editedOption = getRepoEditOptionFromRepo(repo1edited)
244+
assert.Equal(t, *repo1editedOption.Description, *repoEditOption.Description)
245+
assert.Equal(t, *repo1editedOption.HasIssues, true)
246+
assert.NotNil(t, *repo1editedOption.ExternalTracker)
247+
assert.Equal(t, *repo1editedOption.HasWiki, true)
248+
assert.NotNil(t, *repo1editedOption.ExternalWiki)
249+
146250
// reset repo in db
147251
url = fmt.Sprintf("/api/v1/repos/%s/%s?token=%s", user2.Name, *repoEditOption.Name, token2)
148252
req = NewRequestWithJSON(t, "PATCH", url, &origRepoEditOption)

models/repo.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,35 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
275275
}
276276
}
277277
hasIssues := false
278-
if _, err := repo.getUnit(e, UnitTypeIssues); err == nil {
278+
var externalTracker *api.ExternalTracker
279+
var internalTracker *api.InternalTracker
280+
if unit, err := repo.getUnit(e, UnitTypeIssues); err == nil {
281+
config := unit.IssuesConfig()
279282
hasIssues = true
283+
internalTracker = &api.InternalTracker{
284+
EnableTimeTracker: config.EnableTimetracker,
285+
AllowOnlyContributorsToTrackTime: config.AllowOnlyContributorsToTrackTime,
286+
EnableIssueDependencies: config.EnableDependencies,
287+
}
288+
} else if unit, err := repo.getUnit(e, UnitTypeExternalTracker); err == nil {
289+
config := unit.ExternalTrackerConfig()
290+
hasIssues = true
291+
externalTracker = &api.ExternalTracker{
292+
ExternalTrackerURL: config.ExternalTrackerURL,
293+
ExternalTrackerFormat: config.ExternalTrackerFormat,
294+
ExternalTrackerStyle: config.ExternalTrackerStyle,
295+
}
280296
}
281297
hasWiki := false
298+
var externalWiki *api.ExternalWiki
282299
if _, err := repo.getUnit(e, UnitTypeWiki); err == nil {
283300
hasWiki = true
301+
} else if unit, err := repo.getUnit(e, UnitTypeExternalWiki); err == nil {
302+
hasWiki = true
303+
config := unit.ExternalWikiConfig()
304+
externalWiki = &api.ExternalWiki{
305+
ExternalWikiURL: config.ExternalWikiURL,
306+
}
284307
}
285308
hasPullRequests := false
286309
ignoreWhitespaceConflicts := false
@@ -324,7 +347,10 @@ func (repo *Repository) innerAPIFormat(e Engine, mode AccessMode, isParent bool)
324347
Updated: repo.UpdatedUnix.AsTime(),
325348
Permissions: permission,
326349
HasIssues: hasIssues,
350+
ExternalTracker: externalTracker,
351+
InternalTracker: internalTracker,
327352
HasWiki: hasWiki,
353+
ExternalWiki: externalWiki,
328354
HasPullRequests: hasPullRequests,
329355
IgnoreWhitespaceConflicts: ignoreWhitespaceConflicts,
330356
AllowMerge: allowMerge,

modules/structs/repo.go

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,35 @@ type Permission struct {
1515
Pull bool `json:"pull"`
1616
}
1717

18+
// InternalTracker represents settings for internal tracker
19+
// swagger:model
20+
type InternalTracker struct {
21+
// Enable time tracking (Built-in issue tracker)
22+
EnableTimeTracker bool `json:"enable_time_tracker"`
23+
// Let only contributors track time (Built-in issue tracker)
24+
AllowOnlyContributorsToTrackTime bool `json:"allow_only_contributors_to_track_time"`
25+
// Enable dependencies for issues and pull requests (Built-in issue tracker)
26+
EnableIssueDependencies bool `json:"enable_issue_dependencies"`
27+
}
28+
29+
// ExternalTracker represents settings for external tracker
30+
// swagger:model
31+
type ExternalTracker struct {
32+
// URL of external issue tracker.
33+
ExternalTrackerURL string `json:"external_tracker_url"`
34+
// External Issue Tracker URL Format. Use the placeholders {user}, {repo} and {index} for the username, repository name and issue index.
35+
ExternalTrackerFormat string `json:"external_tracker_format"`
36+
// External Issue Tracker Number Format, either `numeric` or `alphanumeric`
37+
ExternalTrackerStyle string `json:"external_tracker_style"`
38+
}
39+
40+
// ExternalWiki represents setting for external wiki
41+
// swagger:model
42+
type ExternalWiki struct {
43+
// URL of external wiki.
44+
ExternalWikiURL string `json:"external_wiki_url"`
45+
}
46+
1847
// Repository represents a repository
1948
type Repository struct {
2049
ID int64 `json:"id"`
@@ -42,17 +71,20 @@ type Repository struct {
4271
// swagger:strfmt date-time
4372
Created time.Time `json:"created_at"`
4473
// swagger:strfmt date-time
45-
Updated time.Time `json:"updated_at"`
46-
Permissions *Permission `json:"permissions,omitempty"`
47-
HasIssues bool `json:"has_issues"`
48-
HasWiki bool `json:"has_wiki"`
49-
HasPullRequests bool `json:"has_pull_requests"`
50-
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
51-
AllowMerge bool `json:"allow_merge_commits"`
52-
AllowRebase bool `json:"allow_rebase"`
53-
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
54-
AllowSquash bool `json:"allow_squash_merge"`
55-
AvatarURL string `json:"avatar_url"`
74+
Updated time.Time `json:"updated_at"`
75+
Permissions *Permission `json:"permissions,omitempty"`
76+
HasIssues bool `json:"has_issues"`
77+
InternalTracker *InternalTracker `json:"internal_tracker,omitempty"`
78+
ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"`
79+
HasWiki bool `json:"has_wiki"`
80+
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
81+
HasPullRequests bool `json:"has_pull_requests"`
82+
IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"`
83+
AllowMerge bool `json:"allow_merge_commits"`
84+
AllowRebase bool `json:"allow_rebase"`
85+
AllowRebaseMerge bool `json:"allow_rebase_explicit"`
86+
AllowSquash bool `json:"allow_squash_merge"`
87+
AvatarURL string `json:"avatar_url"`
5688
}
5789

5890
// CreateRepoOption options when creating repository
@@ -95,8 +127,14 @@ type EditRepoOption struct {
95127
Private *bool `json:"private,omitempty"`
96128
// either `true` to enable issues for this repository or `false` to disable them.
97129
HasIssues *bool `json:"has_issues,omitempty"`
130+
// set this structure to configure internal issue tracker (requires has_issues)
131+
InternalTracker *InternalTracker `json:"internal_tracker,omitempty"`
132+
// set this structure to use external issue tracker (requires has_issues)
133+
ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"`
98134
// either `true` to enable the wiki for this repository or `false` to disable it.
99135
HasWiki *bool `json:"has_wiki,omitempty"`
136+
// set this structure to use external wiki instead of internal (requires has_wiki)
137+
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
100138
// sets the default branch for this repository.
101139
DefaultBranch *string `json:"default_branch,omitempty"`
102140
// either `true` to allow pull requests, or `false` to prevent pull request.

routers/api/v1/repo/repo.go

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"code.gitea.io/gitea/modules/setting"
2020
api "code.gitea.io/gitea/modules/structs"
2121
"code.gitea.io/gitea/modules/util"
22+
"code.gitea.io/gitea/modules/validation"
2223
"code.gitea.io/gitea/routers/api/v1/convert"
2324
mirror_service "code.gitea.io/gitea/services/mirror"
2425
)
@@ -669,27 +670,56 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
669670
units = append(units, *unit)
670671
}
671672
} else if *opts.HasIssues {
672-
// We don't currently allow setting individual issue settings through the API,
673-
// only can enable/disable issues, so when enabling issues,
674-
// we either get the existing config which means it was already enabled,
675-
// or create a new config since it doesn't exist.
676-
unit, err := repo.GetUnit(models.UnitTypeIssues)
677-
var config *models.IssuesConfig
678-
if err != nil {
679-
// Unit type doesn't exist so we make a new config file with default values
680-
config = &models.IssuesConfig{
681-
EnableTimetracker: true,
682-
AllowOnlyContributorsToTrackTime: true,
683-
EnableDependencies: true,
673+
if opts.ExternalTracker != nil {
674+
675+
// Check that values are valid
676+
if !validation.IsValidExternalURL(opts.ExternalTracker.ExternalTrackerURL) {
677+
err := fmt.Errorf("External tracker URL not valid")
678+
ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL", err)
679+
return err
684680
}
681+
if len(opts.ExternalTracker.ExternalTrackerFormat) != 0 && !validation.IsValidExternalTrackerURLFormat(opts.ExternalTracker.ExternalTrackerFormat) {
682+
err := fmt.Errorf("External tracker URL format not valid")
683+
ctx.Error(http.StatusUnprocessableEntity, "Invalid external tracker URL format", err)
684+
return err
685+
}
686+
687+
units = append(units, models.RepoUnit{
688+
RepoID: repo.ID,
689+
Type: models.UnitTypeExternalTracker,
690+
Config: &models.ExternalTrackerConfig{
691+
ExternalTrackerURL: opts.ExternalTracker.ExternalTrackerURL,
692+
ExternalTrackerFormat: opts.ExternalTracker.ExternalTrackerFormat,
693+
ExternalTrackerStyle: opts.ExternalTracker.ExternalTrackerStyle,
694+
},
695+
})
685696
} else {
686-
config = unit.IssuesConfig()
697+
// Default to built-in tracker
698+
var config *models.IssuesConfig
699+
700+
if opts.InternalTracker != nil {
701+
config = &models.IssuesConfig{
702+
EnableTimetracker: opts.InternalTracker.EnableTimeTracker,
703+
AllowOnlyContributorsToTrackTime: opts.InternalTracker.AllowOnlyContributorsToTrackTime,
704+
EnableDependencies: opts.InternalTracker.EnableIssueDependencies,
705+
}
706+
} else if unit, err := repo.GetUnit(models.UnitTypeIssues); err != nil {
707+
// Unit type doesn't exist so we make a new config file with default values
708+
config = &models.IssuesConfig{
709+
EnableTimetracker: true,
710+
AllowOnlyContributorsToTrackTime: true,
711+
EnableDependencies: true,
712+
}
713+
} else {
714+
config = unit.IssuesConfig()
715+
}
716+
717+
units = append(units, models.RepoUnit{
718+
RepoID: repo.ID,
719+
Type: models.UnitTypeIssues,
720+
Config: config,
721+
})
687722
}
688-
units = append(units, models.RepoUnit{
689-
RepoID: repo.ID,
690-
Type: models.UnitTypeIssues,
691-
Config: config,
692-
})
693723
}
694724

695725
if opts.HasWiki == nil {
@@ -700,16 +730,30 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
700730
units = append(units, *unit)
701731
}
702732
} else if *opts.HasWiki {
703-
// We don't currently allow setting individual wiki settings through the API,
704-
// only can enable/disable the wiki, so when enabling the wiki,
705-
// we either get the existing config which means it was already enabled,
706-
// or create a new config since it doesn't exist.
707-
config := &models.UnitConfig{}
708-
units = append(units, models.RepoUnit{
709-
RepoID: repo.ID,
710-
Type: models.UnitTypeWiki,
711-
Config: config,
712-
})
733+
if opts.ExternalWiki != nil {
734+
735+
// Check that values are valid
736+
if !validation.IsValidExternalURL(opts.ExternalWiki.ExternalWikiURL) {
737+
err := fmt.Errorf("External wiki URL not valid")
738+
ctx.Error(http.StatusUnprocessableEntity, "", "Invalid external wiki URL")
739+
return err
740+
}
741+
742+
units = append(units, models.RepoUnit{
743+
RepoID: repo.ID,
744+
Type: models.UnitTypeExternalWiki,
745+
Config: &models.ExternalWikiConfig{
746+
ExternalWikiURL: opts.ExternalWiki.ExternalWikiURL,
747+
},
748+
})
749+
} else {
750+
config := &models.UnitConfig{}
751+
units = append(units, models.RepoUnit{
752+
RepoID: repo.ID,
753+
Type: models.UnitTypeWiki,
754+
Config: config,
755+
})
756+
}
713757
}
714758

715759
if opts.HasPullRequests == nil {

0 commit comments

Comments
 (0)