Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 20a60c4

Browse files
author
Noah Lee
authored
Provide to update a repository name by API (#419)
* Fix the API to update the name of a repository * Fix the message for the 'repo_unique_name' * Add the name field to update the repository name. * Fix lint
1 parent be0132d commit 20a60c4

File tree

10 files changed

+177
-65
lines changed

10 files changed

+177
-65
lines changed

internal/pkg/store/repo.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,13 @@ func (s *Store) SyncRepo(ctx context.Context, r *extent.RemoteRepo) (*ent.Repo,
179179
func (s *Store) UpdateRepo(ctx context.Context, r *ent.Repo) (*ent.Repo, error) {
180180
ret, err := s.c.Repo.
181181
UpdateOne(r).
182+
SetName(r.Name).
182183
SetConfigPath(r.ConfigPath).
183184
Save(ctx)
184185
if ent.IsValidationError(err) {
185-
return nil, e.NewErrorWithMessage(
186-
e.ErrorCodeEntityUnprocessable,
187-
fmt.Sprintf("The value of \"%s\" field is invalid.", err.(*ent.ValidationError).Name),
188-
err)
186+
return nil, e.NewErrorWithMessage(e.ErrorCodeEntityUnprocessable, fmt.Sprintf("The value of \"%s\" field is invalid.", err.(*ent.ValidationError).Name), err)
187+
} else if ent.IsConstraintError(err) {
188+
return nil, e.NewError(e.ErrorRepoUniqueName, err)
189189
} else if err != nil {
190190
return nil, e.NewError(e.ErrorCodeInternalError, err)
191191
}

internal/pkg/store/repo_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/gitploy-io/gitploy/model/ent/enttest"
1010
"github.com/gitploy-io/gitploy/model/ent/migrate"
1111
"github.com/gitploy-io/gitploy/model/extent"
12+
"github.com/gitploy-io/gitploy/pkg/e"
1213
)
1314

1415
func TestStore_ListReposOfUser(t *testing.T) {
@@ -197,6 +198,72 @@ func TestStore_SyncRepo(t *testing.T) {
197198
})
198199
}
199200

201+
func TestStore_UpdateRepo(t *testing.T) {
202+
t.Run("Update the repository name.", func(t *testing.T) {
203+
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1",
204+
enttest.WithMigrateOptions(migrate.WithForeignKeys(false)),
205+
)
206+
defer client.Close()
207+
208+
repo := client.Repo.Create().
209+
SetNamespace("gitploy-io").
210+
SetName("gitploy").
211+
SetDescription("").
212+
SaveX(context.Background())
213+
214+
s := NewStore(client)
215+
216+
// Replace values
217+
repo.Name = "gitploy-next"
218+
repo.ConfigPath = "deploy-next.yml"
219+
220+
var (
221+
ret *ent.Repo
222+
err error
223+
)
224+
ret, err = s.UpdateRepo(context.Background(), repo)
225+
if err != nil {
226+
t.Fatalf("UpdateRepo return an error: %s", err)
227+
}
228+
229+
if repo.Name != "gitploy-next" ||
230+
repo.ConfigPath != "deploy-next.yml" {
231+
t.Fatalf("UpdateRepo = %s, wanted %s", repo, ret)
232+
}
233+
})
234+
235+
t.Run("Return an error if the same repository name exists.", func(t *testing.T) {
236+
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1",
237+
enttest.WithMigrateOptions(migrate.WithForeignKeys(true)),
238+
)
239+
defer client.Close()
240+
241+
client.Repo.Create().
242+
SetNamespace("gitploy-io").
243+
SetName("gitploy-next").
244+
SetDescription("").
245+
SaveX(context.Background())
246+
247+
repo := client.Repo.Create().
248+
SetNamespace("gitploy-io").
249+
SetName("gitploy").
250+
SetDescription("").
251+
SaveX(context.Background())
252+
253+
s := NewStore(client)
254+
255+
repo.Name = "gitploy-next"
256+
257+
var (
258+
err error
259+
)
260+
_, err = s.UpdateRepo(context.Background(), repo)
261+
if !e.HasErrorCode(err, e.ErrorRepoUniqueName) {
262+
t.Fatalf("UpdateRepo doesn't return the ErrorRepoUniqueName error: %s", err)
263+
}
264+
})
265+
}
266+
200267
func TestStore_Activate(t *testing.T) {
201268
t.Run("Update webhook ID and owner ID.", func(t *testing.T) {
202269
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1",

internal/server/api/v1/repos/repo_update.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
type (
1616
RepoPatchPayload struct {
17+
Name *string `json:"name"`
1718
ConfigPath *string `json:"config_path"`
1819
Active *bool `json:"active"`
1920
}
@@ -57,17 +58,22 @@ func (s *RepoAPI) Update(c *gin.Context) {
5758
}
5859
}
5960

61+
if p.Name != nil {
62+
s.log.Debug("Set the name field.", zap.String("value", *p.Name))
63+
re.Name = *p.Name
64+
}
65+
6066
if p.ConfigPath != nil {
61-
if *p.ConfigPath != re.ConfigPath {
62-
re.ConfigPath = *p.ConfigPath
67+
s.log.Debug("Set the config_path field.", zap.String("value", *p.ConfigPath))
68+
re.ConfigPath = *p.ConfigPath
69+
}
6370

64-
if re, err = s.i.UpdateRepo(ctx, re); err != nil {
65-
s.log.Check(gb.GetZapLogLevel(err), "Failed to update the repository.").Write(zap.Error(err))
66-
gb.ResponseWithError(c, err)
67-
return
68-
}
69-
}
71+
if re, err = s.i.UpdateRepo(ctx, re); err != nil {
72+
s.log.Check(gb.GetZapLogLevel(err), "Failed to update the repository.").Write(zap.Error(err))
73+
gb.ResponseWithError(c, err)
74+
return
7075
}
7176

77+
s.log.Info("Update the repository.", zap.Int64("repo_id", re.ID))
7278
gb.Response(c, http.StatusOK, re)
7379
}

internal/server/api/v1/repos/repo_update_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ func TestRepoAPI_UpdateRepo(t *testing.T) {
9393
return r, nil
9494
})
9595

96+
m.EXPECT().
97+
UpdateRepo(gomock.Any(), gomock.AssignableToTypeOf(&ent.Repo{})).
98+
DoAndReturn(func(ctx context.Context, r *ent.Repo) (*ent.Repo, error) {
99+
return r, nil
100+
})
101+
96102
gin.SetMode(gin.ReleaseMode)
97103
router := gin.New()
98104

pkg/e/code.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ const (
4949

5050
// ErrorPermissionRequired is the permission is required to access.
5151
ErrorPermissionRequired ErrorCode = "permission_required"
52+
53+
// ErrorRepoUniqueName is the repository name must be unique.
54+
ErrorRepoUniqueName ErrorCode = "repo_unique_name"
5255
)
5356

5457
type (

pkg/e/trans.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ var messages = map[ErrorCode]string{
1919
ErrorCodeLicenseDecode: "Decoding the license is failed.",
2020
ErrorCodeLicenseRequired: "The license is required.",
2121
ErrorCodeParameterInvalid: "Invalid request parameter.",
22-
ErrorPermissionRequired: "The permission is required",
22+
ErrorPermissionRequired: "The permission is required.",
23+
ErrorRepoUniqueName: "The same repository name already exists.",
2324
}
2425

2526
func GetMessage(code ErrorCode) string {
@@ -49,6 +50,7 @@ var httpCodes = map[ErrorCode]int{
4950
ErrorCodeLicenseRequired: http.StatusPaymentRequired,
5051
ErrorCodeParameterInvalid: http.StatusBadRequest,
5152
ErrorPermissionRequired: http.StatusForbidden,
53+
ErrorRepoUniqueName: http.StatusUnprocessableEntity,
5254
}
5355

5456
func GetHttpCode(code ErrorCode) int {

ui/src/apis/repo.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { instance, headers } from './setting'
44
import { _fetch } from "./_base"
55
import { DeploymentData, mapDataToDeployment } from "./deployment"
66

7-
import { Repo, HttpForbiddenError, Deployment } from '../models'
7+
import { Repo, HttpForbiddenError, Deployment, HttpUnprocessableEntityError } from '../models'
88

99
export interface RepoData {
1010
id: number
@@ -66,7 +66,10 @@ export const getRepo = async (namespace: string, name: string): Promise<Repo> =>
6666
return repo
6767
}
6868

69-
export const updateRepo = async (namespace: string, name: string, payload: {config_path: string}): Promise<Repo> => {
69+
export const updateRepo = async (namespace: string, name: string, payload: {
70+
name?: string,
71+
config_path?: string,
72+
}): Promise<Repo> => {
7073
const res = await _fetch(`${instance}/api/v1/repos/${namespace}/${name}`, {
7174
headers,
7275
credentials: 'same-origin',
@@ -76,6 +79,9 @@ export const updateRepo = async (namespace: string, name: string, payload: {conf
7679
if (res.status === StatusCodes.FORBIDDEN) {
7780
const message = await res.json().then(data => data.message)
7881
throw new HttpForbiddenError(message)
82+
} else if (res.status === StatusCodes.UNPROCESSABLE_ENTITY) {
83+
const message = await res.json().then(data => data.message)
84+
throw new HttpUnprocessableEntityError(message)
7985
}
8086

8187
const ret: Repo = await res

ui/src/redux/repoSettings.ts renamed to ui/src/redux/repoSettings.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit"
1+
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
22
import { message } from "antd"
33

44
import { getRepo, updateRepo, deactivateRepo } from "../apis"
5-
import { Repo, RequestStatus, HttpForbiddenError } from "../models"
5+
import { Repo, RequestStatus, HttpForbiddenError, HttpUnprocessableEntityError } from "../models"
66

77
interface RepoSettingsState {
88
repo?: Repo
@@ -25,9 +25,16 @@ export const init = createAsyncThunk<Repo, {namespace: string, name: string}, {
2525
},
2626
)
2727

28-
export const save = createAsyncThunk<Repo, void, { state: {repoSettings: RepoSettingsState} }>(
28+
export const save = createAsyncThunk<
29+
Repo,
30+
{
31+
name: string,
32+
config_path: string
33+
},
34+
{ state: {repoSettings: RepoSettingsState} }
35+
>(
2936
'repoSettings/save',
30-
async (_, { getState, rejectWithValue, requestId } ) => {
37+
async (values, { getState, rejectWithValue, requestId } ) => {
3138
const { repo, saveId, saving } = getState().repoSettings
3239
if (!repo) {
3340
throw new Error("There is no repo.")
@@ -38,13 +45,19 @@ export const save = createAsyncThunk<Repo, void, { state: {repoSettings: RepoSet
3845
}
3946

4047
try {
41-
const nr = await updateRepo(repo.namespace, repo.name, {config_path: repo.configPath})
48+
const nr = await updateRepo(repo.namespace, repo.name, values)
4249
message.success("Success to save.", 3)
4350
return nr
4451
} catch(e) {
4552
if (e instanceof HttpForbiddenError) {
4653
message.warn("Only admin permission can update.", 3)
47-
}
54+
} else if (e instanceof HttpUnprocessableEntityError) {
55+
message.error(<>
56+
<span>It is unprocesable entity.</span><br/>
57+
<span className="gitploy-quote">{e.message}</span>
58+
</>, 3)
59+
}
60+
4861

4962
return rejectWithValue(e)
5063
}
@@ -65,7 +78,6 @@ export const deactivate = createAsyncThunk<Repo, void, { state: {repoSettings: R
6578
if (e instanceof HttpForbiddenError) {
6679
message.warn("Only admin permission can deactivate.", 3)
6780
}
68-
6981
return rejectWithValue(e)
7082
}
7183
},
@@ -74,15 +86,7 @@ export const deactivate = createAsyncThunk<Repo, void, { state: {repoSettings: R
7486
export const repoSettingsSlice = createSlice({
7587
name: "repoSettings",
7688
initialState,
77-
reducers: {
78-
setConfigPath: (state, action: PayloadAction<string>) => {
79-
if (!state.repo) {
80-
return
81-
}
82-
83-
state.repo.configPath = action.payload
84-
}
85-
},
89+
reducers: {},
8690
extraReducers: builder => {
8791
builder
8892
.addCase(init.fulfilled, (state, action) => {

ui/src/views/repoSettings/SettingsForm.tsx

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,68 @@
11
import { Form, Input, Button, Space, Typography } from "antd"
2-
3-
import { Repo } from "../../models"
2+
import { useState } from "react"
43

54
export interface SettingFormProps {
6-
saving: boolean
7-
repo?: Repo
8-
onClickFinish(values: any): void
5+
configLink: string
6+
initialValues?: SettingFormValues
7+
onClickFinish(values: SettingFormValues): void
98
onClickDeactivate(): void
109
}
1110

11+
export interface SettingFormValues {
12+
name: string
13+
config_path: string
14+
}
15+
1216
export default function SettingForm({
13-
saving,
14-
repo,
17+
configLink,
18+
initialValues,
1519
onClickFinish,
1620
onClickDeactivate,
1721
}: SettingFormProps): JSX.Element {
22+
const [saving, setSaving] = useState(false)
23+
1824
const layout = {
1925
labelCol: { span: 5},
2026
wrapperCol: { span: 12 },
21-
};
27+
}
2228

2329
const submitLayout = {
2430
wrapperCol: { offset: 5, span: 12 },
25-
};
31+
}
2632

27-
const initialValues = {
28-
"config": repo?.configPath
33+
const onFinish = (values: any) => {
34+
setSaving(true)
35+
onClickFinish(values)
36+
setSaving(false)
2937
}
3038

3139
return (
3240
<Form
3341
name="setting"
3442
initialValues={initialValues}
35-
onFinish={onClickFinish}
43+
onFinish={onFinish}
3644
>
45+
<Form.Item
46+
label="Name"
47+
name="name"
48+
{...layout}
49+
rules={[{required: true}]}
50+
>
51+
<Input />
52+
</Form.Item>
3753
<Form.Item
3854
label="Config"
3955
{...layout}
4056
>
4157
<Space>
4258
<Form.Item
43-
name="config"
59+
name="config_path"
4460
rules={[{required: true}]}
4561
noStyle
4662
>
4763
<Input />
4864
</Form.Item>
49-
<Typography.Link target="_blank" href={`/link/${repo?.namespace}/${repo?.name}/config`}>
65+
<Typography.Link target="_blank" href={configLink}>
5066
Link
5167
</Typography.Link>
5268
</Space>

0 commit comments

Comments
 (0)