Skip to content

Commit 525f0b6

Browse files
committed
Use git plumbing for upload: go-gitea#5621 repo_editor.go: UploadRepoFile
1 parent f631702 commit 525f0b6

File tree

1 file changed

+261
-46
lines changed

1 file changed

+261
-46
lines changed

models/repo_editor.go

Lines changed: 261 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package models
66

77
import (
8+
"bytes"
9+
"context"
810
"fmt"
911
"io"
1012
"io/ioutil"
@@ -13,6 +15,7 @@ import (
1315
"os/exec"
1416
"path"
1517
"path/filepath"
18+
"strings"
1619
"time"
1720

1821
"github.com/Unknwon/com"
@@ -84,74 +87,286 @@ type UpdateRepoFileOptions struct {
8487
IsNewFile bool
8588
}
8689

87-
// UpdateRepoFile adds or updates a file in repository.
88-
func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) {
89-
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
90-
defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
90+
func (repo *Repository) bareClone(repoPath string, branch string) (err error) {
91+
if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,
92+
fmt.Sprintf("bareClone (git clone -s --bare): %s", repoPath),
93+
"git", "clone", "-s", "--bare", "-b", branch, repo.RepoPath(), repoPath); err != nil {
94+
return fmt.Errorf("bareClone: %v %s", err, stderr)
95+
}
96+
return nil
97+
}
9198

92-
if err = repo.DiscardLocalRepoBranchChanges(opts.OldBranch); err != nil {
93-
return fmt.Errorf("DiscardLocalRepoBranchChanges [branch: %s]: %v", opts.OldBranch, err)
94-
} else if err = repo.UpdateLocalCopyBranch(opts.OldBranch); err != nil {
95-
return fmt.Errorf("UpdateLocalCopyBranch [branch: %s]: %v", opts.OldBranch, err)
99+
func (repo *Repository) setDefaultIndex(repoPath string) (err error) {
100+
if _, stderr, err := process.GetManager().ExecDir(5*time.Minute,
101+
repoPath,
102+
fmt.Sprintf("setDefaultIndex (git read-tree HEAD): %s", repoPath),
103+
"git", "read-tree", "HEAD"); err != nil {
104+
return fmt.Errorf("setDefaultIndex: %v %s", err, stderr)
96105
}
106+
return nil
107+
}
97108

98-
if opts.OldBranch != opts.NewBranch {
99-
if err := repo.CheckoutNewBranch(opts.OldBranch, opts.NewBranch); err != nil {
100-
return fmt.Errorf("CheckoutNewBranch [old_branch: %s, new_branch: %s]: %v", opts.OldBranch, opts.NewBranch, err)
109+
// FIXME: We should probably return the mode too
110+
func (repo *Repository) lsFiles(repoPath string, args ...string) ([]string, error) {
111+
stdOut := new(bytes.Buffer)
112+
stdErr := new(bytes.Buffer)
113+
114+
timeout := 5 * time.Minute
115+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
116+
defer cancel()
117+
118+
cmdArgs := []string{"ls-files", "-z", "--"}
119+
for _, arg := range args {
120+
if arg != "" {
121+
cmdArgs = append(cmdArgs, arg)
101122
}
102123
}
103124

104-
localPath := repo.LocalCopyPath()
105-
oldFilePath := path.Join(localPath, opts.OldTreeName)
106-
filePath := path.Join(localPath, opts.NewTreeName)
107-
dir := path.Dir(filePath)
125+
cmd := exec.CommandContext(ctx, "git", cmdArgs...)
126+
desc := fmt.Sprintf("lsFiles: (git ls-files) %v", cmdArgs)
127+
cmd.Dir = repoPath
128+
cmd.Stdout = stdOut
129+
cmd.Stderr = stdErr
108130

109-
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
110-
return fmt.Errorf("Failed to create dir %s: %v", dir, err)
131+
if err := cmd.Start(); err != nil {
132+
return nil, fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err())
133+
}
134+
135+
pid := process.GetManager().Add(desc, cmd)
136+
err := cmd.Wait()
137+
process.GetManager().Remove(pid)
138+
139+
if err != nil {
140+
err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
141+
return nil, err
142+
}
143+
144+
filelist := make([]string, len(args))
145+
for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) {
146+
filelist = append(filelist, string(line))
147+
}
148+
149+
return filelist, err
150+
}
151+
152+
func (repo *Repository) removeFilesFromIndex(repoPath string, args ...string) error {
153+
stdOut := new(bytes.Buffer)
154+
stdErr := new(bytes.Buffer)
155+
stdIn := new(bytes.Buffer)
156+
for _, file := range args {
157+
if file != "" {
158+
stdIn.WriteString("0 0000000000000000000000000000000000000000\t")
159+
stdIn.WriteString(file)
160+
stdIn.WriteByte('\000')
161+
}
162+
}
163+
164+
timeout := 5 * time.Minute
165+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
166+
defer cancel()
167+
168+
cmdArgs := []string{"update-index", "--remove", "-z", "--index-info"}
169+
cmd := exec.CommandContext(ctx, "git", cmdArgs...)
170+
desc := fmt.Sprintf("removeFilesFromIndex: (git update-index) %v", args)
171+
cmd.Dir = repoPath
172+
cmd.Stdout = stdOut
173+
cmd.Stderr = stdErr
174+
cmd.Stdin = bytes.NewReader(stdIn.Bytes())
175+
176+
if err := cmd.Start(); err != nil {
177+
return fmt.Errorf("exec(%s) failed: %v(%v)", desc, err, ctx.Err())
178+
}
179+
180+
pid := process.GetManager().Add(desc, cmd)
181+
err := cmd.Wait()
182+
process.GetManager().Remove(pid)
183+
184+
if err != nil {
185+
err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOut, stdErr)
186+
}
187+
188+
return err
189+
}
190+
191+
func (repo *Repository) hashObject(repoPath string, content io.Reader) (string, error) {
192+
timeout := 5 * time.Minute
193+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
194+
defer cancel()
195+
196+
hashCmd := exec.CommandContext(ctx, "git", "hash-object", "-w", "--stdin")
197+
hashCmd.Dir = repoPath
198+
hashCmd.Stdin = content
199+
stdOutBuffer := new(bytes.Buffer)
200+
stdErrBuffer := new(bytes.Buffer)
201+
hashCmd.Stdout = stdOutBuffer
202+
hashCmd.Stderr = stdErrBuffer
203+
desc := fmt.Sprintf("hashObject: (git hash-object)")
204+
if err := hashCmd.Start(); err != nil {
205+
return "", fmt.Errorf("git hash-object: %s", err)
206+
}
207+
208+
pid := process.GetManager().Add(desc, hashCmd)
209+
err := hashCmd.Wait()
210+
process.GetManager().Remove(pid)
211+
212+
if err != nil {
213+
err = fmt.Errorf("exec(%d:%s) failed: %v(%v) stdout: %v stderr: %v", pid, desc, err, ctx.Err(), stdOutBuffer, stdErrBuffer)
214+
return "", err
215+
}
216+
217+
return strings.TrimSpace(stdOutBuffer.String()), nil
218+
}
219+
220+
func (repo *Repository) addObjectToIndex(repoPath, mode, objectHash, objectPath string) error {
221+
if _, stderr, err := process.GetManager().ExecDir(5*time.Minute,
222+
repoPath,
223+
fmt.Sprintf("addObjectToIndex (git update-index): %s", repoPath),
224+
"git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil {
225+
return fmt.Errorf("git update-index: %s", stderr)
226+
}
227+
return nil
228+
}
229+
230+
func (repo *Repository) writeTree(repoPath string) (string, error) {
231+
232+
treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute,
233+
repoPath,
234+
fmt.Sprintf("writeTree (git write-tree): %s", repoPath),
235+
"git", "write-tree")
236+
if err != nil {
237+
return "", fmt.Errorf("git write-tree: %s", stderr)
238+
}
239+
return strings.TrimSpace(treeHash), nil
240+
}
241+
242+
func (repo *Repository) commitTree(repoPath string, doer *User, treeHash string, message string) (string, error) {
243+
commitTimeStr := time.Now().Format(time.UnixDate)
244+
245+
// FIXME: Should we add SSH_ORIGINAL_COMMAND to this
246+
// Because this may call hooks we should pass in the environment
247+
env := append(os.Environ(),
248+
"GIT_AUTHOR_NAME="+doer.DisplayName(),
249+
"GIT_AUTHOR_EMAIL="+doer.getEmail(),
250+
"GIT_AUTHOR_DATE="+commitTimeStr,
251+
"GIT_COMMITTER_NAME="+doer.DisplayName(),
252+
"GIT_COMMITTER_EMAIL="+doer.getEmail(),
253+
"GIT_COMMITTER_DATE="+commitTimeStr,
254+
)
255+
commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
256+
repoPath,
257+
fmt.Sprintf("commitTree (git commit-tree): %s", repoPath),
258+
env,
259+
"git", "commit-tree", treeHash, "-p", "HEAD", "-m", message)
260+
if err != nil {
261+
return "", fmt.Errorf("git commit-tree: %s", stderr)
262+
}
263+
return strings.TrimSpace(commitHash), nil
264+
}
265+
266+
func (repo *Repository) actuallyPush(repoPath string, doer *User, commitHash string, branch string) error {
267+
isUncyclo := "false"
268+
if strings.HasSuffix(repo.Name, ".wiki") {
269+
isUncyclo = "true"
270+
}
271+
272+
// FIXME: Should we add SSH_ORIGINAL_COMMAND to this
273+
// Because calls hooks we need to pass in the environment
274+
env := append(os.Environ(),
275+
"GIT_AUTHOR_NAME="+doer.DisplayName(),
276+
"GIT_AUTHOR_EMAIL="+doer.getEmail(),
277+
"GIT_COMMITTER_NAME="+doer.DisplayName(),
278+
"GIT_COMMITTER_EMAIL="+doer.getEmail(),
279+
EnvRepoName+"="+repo.Name,
280+
EnvRepoUsername+"="+repo.OwnerName,
281+
EnvRepoIsUncyclo+"="+isUncyclo,
282+
EnvPusherName+"="+doer.Name,
283+
EnvPusherID+"="+fmt.Sprintf("%d", doer.ID),
284+
ProtectedBranchRepoID+"="+fmt.Sprintf("%d", repo.ID),
285+
)
286+
287+
if _, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
288+
repoPath,
289+
fmt.Sprintf("actuallyPush (git push): %s", repoPath),
290+
env,
291+
"git", "push", repo.RepoPath(), strings.TrimSpace(commitHash)+":refs/heads/"+strings.TrimSpace(branch)); err != nil {
292+
return fmt.Errorf("git push: %s", stderr)
293+
}
294+
return nil
295+
}
296+
297+
// UpdateRepoFile adds or updates a file in the repository.
298+
func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (err error) {
299+
timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
300+
tmpBasePath := path.Join(LocalCopyPath(), "upload-"+timeStr+".git")
301+
if err := os.MkdirAll(path.Dir(tmpBasePath), os.ModePerm); err != nil {
302+
return fmt.Errorf("Failed to create dir %s: %v", tmpBasePath, err)
303+
}
304+
305+
defer os.RemoveAll(path.Dir(tmpBasePath))
306+
307+
// Do a bare shared clone into tmpBasePath and
308+
// make HEAD to point to the OldBranch tree
309+
if err := repo.bareClone(tmpBasePath, opts.OldBranch); err != nil {
310+
return fmt.Errorf("UpdateRepoFile: %v", err)
311+
}
312+
313+
// Set the default index
314+
if err := repo.setDefaultIndex(tmpBasePath); err != nil {
315+
return fmt.Errorf("UpdateRepoFile: %v", err)
316+
}
317+
318+
filesInIndex, err := repo.lsFiles(tmpBasePath, opts.NewTreeName, opts.OldTreeName)
319+
320+
if err != nil {
321+
return fmt.Errorf("UpdateRepoFile: %v", err)
111322
}
112323

113-
// If it's meant to be a new file, make sure it doesn't exist.
114324
if opts.IsNewFile {
115-
if com.IsExist(filePath) {
116-
return ErrRepoFileAlreadyExist{filePath}
325+
for _, file := range filesInIndex {
326+
if file == opts.NewTreeName {
327+
return ErrRepoFileAlreadyExist{opts.NewTreeName}
328+
}
117329
}
118330
}
119331

120-
// Ignore move step if it's a new file under a directory.
121-
// Otherwise, move the file when name changed.
122-
if com.IsFile(oldFilePath) && opts.OldTreeName != opts.NewTreeName {
123-
if err = git.MoveFile(localPath, opts.OldTreeName, opts.NewTreeName); err != nil {
124-
return fmt.Errorf("git mv %s %s: %v", opts.OldTreeName, opts.NewTreeName, err)
332+
//var stdout string
333+
if opts.OldTreeName != opts.NewTreeName && len(filesInIndex) > 0 {
334+
for _, file := range filesInIndex {
335+
if file == opts.OldTreeName {
336+
if err := repo.removeFilesFromIndex(tmpBasePath, opts.OldTreeName); err != nil {
337+
return err
338+
}
339+
}
125340
}
341+
126342
}
127343

128-
if err = ioutil.WriteFile(filePath, []byte(opts.Content), 0666); err != nil {
129-
return fmt.Errorf("WriteFile: %v", err)
344+
// Add the object to the database
345+
objectHash, err := repo.hashObject(tmpBasePath, strings.NewReader(opts.Content))
346+
if err != nil {
347+
return err
130348
}
131349

132-
if err = git.AddChanges(localPath, true); err != nil {
133-
return fmt.Errorf("git add --all: %v", err)
134-
} else if err = git.CommitChanges(localPath, git.CommitChangesOptions{
135-
Committer: doer.NewGitSig(),
136-
Message: opts.Message,
137-
}); err != nil {
138-
return fmt.Errorf("CommitChanges: %v", err)
139-
} else if err = git.Push(localPath, git.PushOptions{
140-
Remote: "origin",
141-
Branch: opts.NewBranch,
142-
}); err != nil {
143-
return fmt.Errorf("git push origin %s: %v", opts.NewBranch, err)
350+
// Add the object to the index
351+
if err := repo.addObjectToIndex(tmpBasePath, "100666", objectHash, opts.NewTreeName); err != nil {
352+
return err
144353
}
145354

146-
gitRepo, err := git.OpenRepository(repo.RepoPath())
355+
// Now write the tree
356+
treeHash, err := repo.writeTree(tmpBasePath)
147357
if err != nil {
148-
log.Error(4, "OpenRepository: %v", err)
149-
return nil
358+
return err
150359
}
151-
commit, err := gitRepo.GetBranchCommit(opts.NewBranch)
360+
361+
// Now commit the tree
362+
commitHash, err := repo.commitTree(tmpBasePath, doer, treeHash, opts.Message)
152363
if err != nil {
153-
log.Error(4, "GetBranchCommit [branch: %s]: %v", opts.NewBranch, err)
154-
return nil
364+
return err
365+
}
366+
367+
// Then push this tree to NewBranch
368+
if err := repo.actuallyPush(tmpBasePath, doer, commitHash, opts.NewBranch); err != nil {
369+
return err
155370
}
156371

157372
// Simulate push event.
@@ -172,7 +387,7 @@ func (repo *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) (
172387
RepoName: repo.Name,
173388
RefFullName: git.BranchPrefix + opts.NewBranch,
174389
OldCommitID: oldCommitID,
175-
NewCommitID: commit.ID.String(),
390+
NewCommitID: commitHash,
176391
},
177392
)
178393
if err != nil {

0 commit comments

Comments
 (0)