Skip to content

Commit 91d9efb

Browse files
authored
Merge pull request #96 from bctnry/master
feat: basic file editing ui
2 parents ff99c23 + 9273232 commit 91d9efb

23 files changed

Lines changed: 716 additions & 466 deletions

docs/single-file-update.org

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
* single file update
2+
3+
currently aegis uses git-fast-import. the process consists of two commands:
4+
5+
+ =git fast-import --date-format=now --quiet=
6+
+ we send the payload to its stdin.
7+
+ we read the content of its stdout.
8+
+ =git update-ref [ref-full-name] [commit-id]=
9+
+ =[ref-full-name]= are the ones that starts with =refs/heads/=, like =refs/heads/master=.
10+
+ =[commit-id]= comes from the stdout from the first command.
11+
12+
** how =git-fast-import= is used
13+
14+
#+begin_src
15+
commit [ref-full-name]
16+
mark :1
17+
author [user-full-name] <[user-email]> now
18+
committer [user-full-name] <[user-email> now
19+
data <<%
20+
[commit message]
21+
%
22+
from [ref-full-name]^0
23+
M [mode] inline [path]
24+
data <<%
25+
data
26+
%
27+
28+
get-mark :1
29+
#+end_src
30+
31+
according to [[https://git-scm.com/docs/git-fast-import][git-fast-import's documentation]]:
32+
33+
+ the =commit= line specifies the branch the new commit needs to be made on.
34+
+ =mark= marks the commit. we need to add a mark here so we can retrieve the id of new commit with =get-mark= command at the last.
35+
+ the next 3 command is for specifying the author, the committer and the commit message.
36+
+ if you need to add a signature, you need to use the separate =gpgsig= command instead of appending the signature to the commit message, since unlike co-authored-by, gpg signature and commit messages are different slots in a commit object.
37+
+ the =from= command is used to specify the parent commit of the created commit. use =[ref-full-name]^0= to specify the latest commit of the branch.
38+
+ the line with =M= is a filemodify command that specifies the creation of a new file or a modification of an existing one.
39+
+ if you use =inline= you need to use a =data= command to specify the data of the resulting file.
40+
+ instead of using =inline= in the =M= command, one can first add the resulting file into the repository as a blob and do =M [mode] [blob-id] [path]= instead.
41+
42+
the =data= command has two forms which I will not describe here. currently aegis use the "delimited" form for commit message and "length-specified" form for text data, since delimited form cannot describe data that doesn't end with line feed.
43+

pkg/aegis/model/acl.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type ACLTuple struct {
1515
ArchiveRepository bool `json:"archiveRepo"`
1616
DeleteRepository bool `json:"deleteRepo"`
1717
EditHooks bool `json:"editHooks"`
18+
EditWebHooks bool `json:"editWebHooks"`
1819
}
1920

2021
type ACL struct {
@@ -26,7 +27,7 @@ func (aclt *ACLTuple) HasSettingPrivilege() bool {
2627
if aclt == nil { return false }
2728
// NOTE THAT having PushToRepository permission does not mean a
2829
// user is allowed to go into the setting panels of things.
29-
return aclt.AddMember || aclt.DeleteMember || aclt.EditMember || aclt.AddRepository || aclt.EditInfo || aclt.ArchiveRepository || aclt.DeleteRepository || aclt.EditHooks
30+
return aclt.AddMember || aclt.DeleteMember || aclt.EditMember || aclt.AddRepository || aclt.EditInfo || aclt.ArchiveRepository || aclt.DeleteRepository || aclt.EditHooks || aclt.EditWebHooks
3031
}
3132

3233
func NewACL() *ACL {
@@ -69,6 +70,7 @@ func ToCommaSeparatedString(aclt *ACLTuple) string {
6970
if aclt.ArchiveRepository { res = append(res, "archiveRepo") }
7071
if aclt.DeleteRepository { res = append(res, "deleteRepo") }
7172
if aclt.EditHooks { res = append(res, "editHooks") }
73+
if aclt.EditWebHooks { res = append(res, "editWebHooks") }
7274
return strings.Join(res, ",")
7375
}
7476

routes/aux.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ func GenerateLoginInfoModel(ctx *RouterContext, r *http.Request) (*templates.Log
8282
return &templates.LoginInfoModel{
8383
LoggedIn: loggedIn,
8484
UserName: "",
85+
UserFullName: "",
86+
UserEmail: "",
8587
IsOwner: false,
8688
IsSettingMember: false,
8789
IsAdmin: false,
@@ -94,6 +96,8 @@ func GenerateLoginInfoModel(ctx *RouterContext, r *http.Request) (*templates.Log
9496
return &templates.LoginInfoModel{
9597
LoggedIn: loggedIn,
9698
UserName: "",
99+
UserFullName: "",
100+
UserEmail: "",
97101
IsOwner: false,
98102
IsSettingMember: false,
99103
IsAdmin: false,
@@ -107,6 +111,8 @@ func GenerateLoginInfoModel(ctx *RouterContext, r *http.Request) (*templates.Log
107111
return &templates.LoginInfoModel{
108112
LoggedIn: res,
109113
UserName: un,
114+
UserFullName: u.Title,
115+
UserEmail: u.Email,
110116
IsOwner: false,
111117
IsSettingMember: false,
112118
IsAdmin: u.Status == model.ADMIN || u.Status == model.SUPER_ADMIN,

routes/context.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ package routes
55
// together and is not used to manage lifetimes & stuff AT ALL.
66

77
import (
8+
"errors"
89
"fmt"
910
"html/template"
1011
"net/http"
1112
"strings"
1213

1314
"github.com/bctnry/aegis/pkg/aegis"
15+
"github.com/bctnry/aegis/pkg/aegis/confirm_code"
1416
"github.com/bctnry/aegis/pkg/aegis/db"
1517
"github.com/bctnry/aegis/pkg/aegis/mail"
1618
"github.com/bctnry/aegis/pkg/aegis/model"
1719
"github.com/bctnry/aegis/pkg/aegis/receipt"
1820
"github.com/bctnry/aegis/pkg/aegis/session"
1921
"github.com/bctnry/aegis/pkg/aegis/ssh"
20-
"github.com/bctnry/aegis/pkg/aegis/confirm_code"
2122
"github.com/bctnry/aegis/templates"
2223
)
2324

@@ -169,6 +170,8 @@ func ParseRepositoryFullName(str string) (string, string) {
169170
return namespaceName, repoName
170171
}
171172

173+
var ErrNotFound = errors.New("Requested object not found")
174+
172175
// we need the namespace acl to supplement the repository acl in terms
173176
// of business logic.
174177
func (ctx *RouterContext) ResolveRepositoryFullName(str string) (string, string, *model.Namespace, *model.Repository, error) {

routes/controller/branch.go

Lines changed: 150 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package controller
22

33
import (
4+
"bytes"
45
"fmt"
56
"mime"
67
"net/http"
8+
"os/exec"
79
"path"
810
"strings"
911

@@ -139,11 +141,13 @@ func bindBranchController(ctx *RouterContext) {
139141
return
140142
}
141143

144+
isTargetBlob := target.Type() == gitlib.BLOB
145+
142146
// if it's a query for snapshot of a tree we directly output
143147
// the tree object as a .zip file
144148
isSnapshotRequest := r.URL.Query().Has("snapshot")
145149
if isSnapshotRequest {
146-
if target.Type() == gitlib.BLOB {
150+
if isTargetBlob {
147151
mime := mime.TypeByExtension(path.Ext(treePath))
148152
if len(mime) <= 0 { mime = "application/octet-stream" }
149153
w.Header().Add("Content-Type", mime)
@@ -179,46 +183,66 @@ func bindBranchController(ctx *RouterContext) {
179183
permaLink := fmt.Sprintf("/repo/%s/commit/%s/%s", rfn, cobj.Id, treePath)
180184

181185
isBlameRequest := r.URL.Query().Has("blame")
182-
if isBlameRequest {
183-
if target.Type() == gitlib.BLOB {
184-
mime := mime.TypeByExtension(path.Ext(treePath))
185-
if len(mime) <= 0 { mime = "application/octet-stream" }
186-
if !strings.HasPrefix(mime, "image/") {
187-
dirPath := path.Dir(treePath) + "/"
188-
dirObj, err := rr.ResolveTreePath(gobj.(*gitlib.TreeObject), dirPath)
189-
if err != nil {
190-
ctx.ReportInternalError(err.Error(), w, r)
191-
return
192-
}
193-
blame, err := rr.Blame(cobj, treePath)
194-
if err != nil {
195-
ctx.ReportInternalError(fmt.Sprintf("Failed to run git-blame: %s.", err), w, r)
196-
return
197-
}
198-
LogTemplateError(ctx.LoadTemplate("git-blame").Execute(w, &templates.GitBlameTemplateModel{
199-
Repository: repo,
200-
RepoHeaderInfo: *repoHeaderInfo,
201-
TreeFileList: &templates.TreeFileListTemplateModel{
202-
ShouldHaveParentLink: len(treePath) > 0,
203-
RepoPath: fmt.Sprintf("/repo/%s", rfn),
204-
RootPath: fmt.Sprintf("/repo/%s/%s/%s", rfn, "branch", branchName),
205-
TreePath: dirPath,
206-
FileList: dirObj.(*gitlib.TreeObject).ObjectList,
207-
},
208-
TreePath: treePathModelValue,
209-
PermaLink: permaLink,
210-
Blame: blame,
211-
CommitInfo: commitInfo,
212-
TagInfo: nil,
213-
LoginInfo: loginInfo,
214-
Config: ctx.Config,
215-
}))
186+
if isBlameRequest && isTargetBlob {
187+
mime := mime.TypeByExtension(path.Ext(treePath))
188+
if len(mime) <= 0 { mime = "application/octet-stream" }
189+
if !strings.HasPrefix(mime, "image/") {
190+
dirPath := path.Dir(treePath) + "/"
191+
dirObj, err := rr.ResolveTreePath(gobj.(*gitlib.TreeObject), dirPath)
192+
if err != nil {
193+
ctx.ReportInternalError(err.Error(), w, r)
194+
return
195+
}
196+
blame, err := rr.Blame(cobj, treePath)
197+
if err != nil {
198+
ctx.ReportInternalError(fmt.Sprintf("Failed to run git-blame: %s.", err), w, r)
216199
return
217200
}
201+
LogTemplateError(ctx.LoadTemplate("git-blame").Execute(w, &templates.GitBlameTemplateModel{
202+
Repository: repo,
203+
RepoHeaderInfo: *repoHeaderInfo,
204+
TreeFileList: &templates.TreeFileListTemplateModel{
205+
ShouldHaveParentLink: len(treePath) > 0,
206+
RepoPath: fmt.Sprintf("/repo/%s", rfn),
207+
RootPath: fmt.Sprintf("/repo/%s/%s/%s", rfn, "branch", branchName),
208+
TreePath: dirPath,
209+
FileList: dirObj.(*gitlib.TreeObject).ObjectList,
210+
},
211+
TreePath: treePathModelValue,
212+
PermaLink: permaLink,
213+
Blame: blame,
214+
CommitInfo: commitInfo,
215+
TagInfo: nil,
216+
LoginInfo: loginInfo,
217+
Config: ctx.Config,
218+
}))
219+
return
218220
}
219221
}
220222

221-
223+
isEditRequest := r.URL.Query().Has("edit")
224+
if isEditRequest && isTargetBlob {
225+
mime := mime.TypeByExtension(path.Ext(treePath))
226+
if len(mime) <= 0 { mime = "application/octet-stream" }
227+
if !strings.HasPrefix(mime, "image/") {
228+
LogTemplateError(ctx.LoadTemplate("edit-file").Execute(w, &templates.EditFileTemplateModel{
229+
Config: ctx.Config,
230+
Repository: repo,
231+
RepoHeaderInfo: *repoHeaderInfo,
232+
PermaLink: permaLink,
233+
FullTreePath: treePath,
234+
FileContent: string(target.(*gitlib.BlobObject).Data),
235+
CommitInfo: commitInfo,
236+
TagInfo: nil,
237+
LoginInfo: loginInfo,
238+
}))
239+
} else {
240+
// TODO: upload.
241+
}
242+
return
243+
}
244+
245+
222246
switch target.Type() {
223247
case gitlib.TREE:
224248
if len(treePath) > 0 && !strings.HasSuffix(treePath, "/") {
@@ -319,8 +343,97 @@ func bindBranchController(ctx *RouterContext) {
319343
default:
320344
ctx.ReportInternalError("", w, r)
321345
}
322-
323346
}))
347+
348+
http.HandleFunc("POST /repo/{repoName}/branch/{branchName}/{treePath...}", UseMiddleware(
349+
[]Middleware{
350+
Logged, LoginRequired, GlobalVisibility,
351+
ValidRepositoryNameRequired("repoName"),
352+
ErrorGuard,
353+
}, ctx,
354+
func(rc *RouterContext, w http.ResponseWriter, r *http.Request) {
355+
rfn := r.PathValue("repoName")
356+
_, _, _, repo, err := rc.ResolveRepositoryFullName(rfn)
357+
if err == routes.ErrNotFound {
358+
rc.ReportNotFound(rfn, "Repository", "Depot", w, r)
359+
return
360+
}
361+
if err != nil {
362+
rc.ReportInternalError(err.Error(), w, r)
363+
return
364+
}
365+
if repo.Type != model.REPO_TYPE_GIT {
366+
rc.ReportNormalError("The repository you have requested isn't a Git repository.", w, r)
367+
return
368+
}
369+
if rc.Config.PlainMode {
370+
FoundAt(w, fmt.Sprintf("/repo/%s/branch/%s/%s", rfn, r.PathValue("branchName"), r.PathValue("treePath")))
371+
return
372+
}
373+
err = r.ParseForm()
374+
if err != nil {
375+
rc.ReportNormalError("Invalid request", w, r)
376+
return
377+
}
378+
if repo.Owner != rc.LoginInfo.UserName {
379+
rc.ReportRedirect(
380+
fmt.Sprintf("/repo/%s/branch/%s/%s", rfn, r.PathValue("branchName"), r.PathValue("treePath")),
381+
5,
382+
"Not Enough Privilege",
383+
"Your account doesn't have enough privilege to perform this action.",
384+
w, r,
385+
)
386+
}
387+
commitMessage := r.Form.Get("commit-message")
388+
content := r.Form.Get("content")
389+
branchName := r.PathValue("branchName")
390+
treePath := r.PathValue("treePath")
391+
payload := fmt.Sprintf(`commit refs/heads/%s
392+
mark :1
393+
author %s <%s> now
394+
committer %s <%s> now
395+
data %d
396+
%s
397+
from refs/heads/%s^0
398+
M 100644 inline %s
399+
data %d
400+
%s
401+
get-mark :1`,
402+
branchName,
403+
rc.LoginInfo.UserFullName, rc.LoginInfo.UserEmail,
404+
rc.LoginInfo.UserFullName, rc.LoginInfo.UserEmail,
405+
len(commitMessage),
406+
commitMessage,
407+
branchName,
408+
treePath,
409+
len(content),
410+
content,
411+
)
412+
cmd := exec.Command("git", "fast-import", "--date-format=now", "--quiet")
413+
stdoutBuf := new(bytes.Buffer)
414+
stderrBuf := new(bytes.Buffer)
415+
cmd.Dir = repo.LocalPath
416+
cmd.Stdout = stdoutBuf
417+
cmd.Stderr = stderrBuf
418+
cmd.Stdin = bytes.NewReader([]byte(payload))
419+
err = cmd.Run()
420+
if err != nil {
421+
rc.ReportInternalError(fmt.Sprintf("Failed to fast-import commit: %s; %s", err.Error(), stderrBuf.String()), w, r)
422+
return
423+
}
424+
commitId := strings.TrimSpace(stdoutBuf.String())
425+
cmd2 := exec.Command("git", "update-ref", fmt.Sprintf("refs/heads/%s", branchName), commitId)
426+
cmd2.Dir = repo.LocalPath
427+
stderrBuf.Reset()
428+
cmd2.Stderr = stderrBuf
429+
err = cmd2.Run()
430+
if err != nil {
431+
rc.ReportInternalError(fmt.Sprintf("Failed to update ref: %s; %s", err.Error(), stderrBuf.String()), w, r)
432+
return
433+
}
434+
rc.ReportRedirect(fmt.Sprintf("/repo/%s/branch/%s/%s", rfn, branchName, treePath), 5, "Updated", "Your edit has been saved to the repository.", w, r)
435+
},
436+
))
324437
}
325438

326439

routes/controller/namespace-setting.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ func bindNamespaceSettingController(ctx *RouterContext) {
536536
PushToRepository: len(r.Form.Get("pushToRepo")) > 0,
537537
ArchiveRepository: len(r.Form.Get("archiveRepo")) > 0,
538538
DeleteRepository: len(r.Form.Get("deleteRepo")) > 0,
539-
EditHooks: len(r.Form.Get("editHooks")) > 0,
539+
EditWebHooks: len(r.Form.Get("editWebHooks")) > 0,
540540
}
541541
err = ctx.DatabaseInterface.SetNamespaceACL(namespaceName, targetUsername, t)
542542
if err != nil {

0 commit comments

Comments
 (0)