55package models
66
77import (
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+ isWiki := "false"
268+ if strings .HasSuffix (repo .Name , ".wiki" ) {
269+ isWiki = "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+ EnvRepoIsWiki + "=" + isWiki ,
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