Browse Source

Squashed commit of the following:

commit 0afcb843d7ffd596991c4885cab768273a6eb42c
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 17:13:29 2016 -0600

    Removed Upload stats as the upload table is just a temporary table

commit 7ecd73ff5535612d79d471409173ee7f1fcfa157
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 08:42:41 2016 -0600

    Fix for CodeMirror mode

commit c29b9ab531e2e7af0fb5db24dc17e51027dd1174
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 08:03:33 2016 -0600

    Made tabbing in editor use spaces

commit 23af384c53206a8a40e11e45bf49d7a149c4adcd
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 07:56:46 2016 -0600

    Fix for data-url

commit cfb8a97591cb6fc0a92e49563b7b764c524db0e9
Merge: 7fc8a89 991ce42
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 07:42:53 2016 -0600

    Merge remote-tracking branch 'gogits/develop' into feature-create-and-edit-repo-file

    Conflicts:
    	modules/bindata/bindata.go
    	public/js/gogs.js

commit 7fc8a89cb495478225b02d613e647f99a1489634
Merge: fd3d86c c03d040
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 07:40:00 2016 -0600

    Merge branch 'feature-create-and-edit-repo-file' of github.com:richmahn/gogs into feature-create-and-edit-repo-file

commit fd3d86ca6bbc02cfda566a504ffd6b03db4f75ef
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Sun Jul 31 07:39:44 2016 -0600

    Code cleanup

commit c03d0401c1049eeeccc32ab1f9c3303c130be5ee
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Fri Jul 29 15:38:23 2016 -0600

    Code cleanup

commit 98e1206ccf9f9a4503c020e3a7830cf9f861dfae
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Thu Jul 28 18:36:01 2016 -0600

    Code cleanup and fixes

commit c2895dc742f25f8412879c9fa15e18f27f42f194
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Thu Jul 28 18:24:04 2016 -0600

    Fixes per Unknwon's requests

commit 6aa7e46b21ad4c96e562daa2eac26a8fb408f8ef
Merge: 889e9fa ad7ea88
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Thu Jul 28 17:13:43 2016 -0600

    Merge remote-tracking branch 'gogits/develop' into feature-create-and-edit-repo-file

    Conflicts:
    	modules/bindata/bindata.go
    	modules/setting/setting.go

commit 889e9faf1bd8559a4979c8f46005d488c1a234d4
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Fri Jul 22 14:09:18 2016 -0600

    Fix in gogs.js

commit 47603edf223f147b114be65f3bd27bc1e88827a5
Merge: bb57912 cf85e9e
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Fri Jul 22 14:07:36 2016 -0600

    Merge remote-tracking branch 'gogits/develop' into feature-create-and-edit-repo-file

    Conflicts:
    	modules/bindata/bindata.go
    	public/js/gogs.js

commit bb5791255867a71c11a77b639db050ad09c597a4
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Fri Jul 22 14:02:18 2016 -0600

    Update for using CodeMirror mode addon

commit d10d128c51039be19e2af9c66c63db66a9f2ec6d
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Tue Jul 19 16:12:57 2016 -0600

    Update for Edit

commit 34a34982025144e3225e389f7849eb6273c1d576
Merge: fa1b752 1c7dcdd
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Tue Jul 19 11:52:02 2016 -0600

    Merge remote-tracking branch 'gogits/develop' into feature-create-and-edit-repo-file

    Conflicts:
    	modules/bindata/bindata.go

commit fa1b752be29cd455c5184ddac2ffe80b3489763e
Author: Richard Mahn <richard_mahn@wycliffeassociates.org>
Date:   Fri Jul 15 18:35:42 2016 -0600

    Feature for editing, creating, uploading and deleting files
Richard Mahn 4 years ago
parent
commit
d0a0239bac

+ 14 - 0
cmd/web.go

@@ -497,6 +497,20 @@ func runWeb(ctx *cli.Context) error {
497 497
 
498 498
 		m.Combo("/compare/*", repo.MustAllowPulls).Get(repo.CompareAndPullRequest).
499 499
 			Post(bindIgnErr(auth.CreateIssueForm{}), repo.CompareAndPullRequestPost)
500
+
501
+		m.Group("", func() {
502
+			m.Combo("/_edit/*").Get(repo.EditFile).
503
+				Post(bindIgnErr(auth.EditRepoFileForm{}), repo.EditFilePost)
504
+			m.Combo("/_new/*").Get(repo.NewFile).
505
+				Post(bindIgnErr(auth.EditRepoFileForm{}), repo.NewFilePost)
506
+			m.Post("/preview/*", bindIgnErr(auth.EditPreviewDiffForm{}), repo.DiffPreviewPost)
507
+			m.Combo("/upload/*").Get(repo.UploadFile).
508
+				Post(bindIgnErr(auth.UploadRepoFileForm{}), repo.UploadFilePost)
509
+			m.Post("/delete/*", bindIgnErr(auth.DeleteRepoFileForm{}), repo.DeleteFilePost)
510
+			m.Post("/branches", bindIgnErr(auth.NewBranchForm{}), repo.NewBranchPost)
511
+			m.Post("/upload-file", repo.UploadFileToServer)
512
+			m.Post("/upload-remove", bindIgnErr(auth.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
513
+		}, context.RepoRef(), context.RepoAssignment(), reqRepoWriter)
500 514
 	}, reqSignIn, context.RepoAssignment(), repo.MustBeNotBare)
501 515
 
502 516
 	m.Group("/:username/:reponame", func() {

+ 23 - 0
conf/app.ini

@@ -20,6 +20,26 @@ MAX_CREATION_LIMIT = -1
20 20
 ; Patch test queue length, make it as large as possible
21 21
 PULL_REQUEST_QUEUE_LENGTH = 10000
22 22
 
23
+[editor]
24
+; List of file extensions that should have line wraps in the CodeMirror editor
25
+; Separate extensions with a comma. To line wrap files w/o extension, just put a comma
26
+LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,
27
+; Valid file modes that have a preview API associated with them, such as api/v1/markdown
28
+; Separate values by commas. Preview tab in edit mode won't show if the file extension doesn't match
29
+PREVIEW_TAB_APIS = markdown
30
+
31
+[upload]
32
+; Whether repository file uploads are enabled. Defaults to `true`
33
+ENABLE_UPLOADS = true
34
+; Path for uploads. Defaults to `data/tmp/uploads` (tmp gets deleted on gogs restart)
35
+TEMP_PATH = data/tmp/uploads
36
+; One or more allowed types, e.g. image/jpeg|image/png. Nothing means any file type
37
+ALLOWED_TYPES =
38
+; Max size of each file in MB. Defaults to 32MB
39
+FILE_MAX_SIZE = 32
40
+; Max number of files per upload. Defaults to 10
41
+MAX_FILES = 10
42
+
23 43
 [ui]
24 44
 ; Number of repositories that are showed in one explore page
25 45
 EXPLORE_PAGING_NUM = 20
@@ -54,6 +74,9 @@ ENABLE_HARD_LINE_BREAK = false
54 74
 ; List of custom URL-Schemes that are allowed as links when rendering Markdown
55 75
 ; for example git,magnet
56 76
 CUSTOM_URL_SCHEMES =
77
+; List of file extensions that should be rendered/edited as Markdown
78
+; Separate extensions with a comma. To render files w/o extension as markdown, just put a comma
79
+MD_FILE_EXTENSIONS = .md,.markdown,.mdown,.mkd
57 80
 
58 81
 [server]
59 82
 PROTOCOL = http

+ 52 - 0
conf/locale/locale_en-US.ini

@@ -189,6 +189,13 @@ TeamName = Team name
189 189
 AuthName = Authorization name
190 190
 AdminEmail = Admin email
191 191
 
192
+NewBranchName = New branch name
193
+CommitSummary = Commit summary
194
+CommitMessage = Commit message
195
+CommitChoice = Commit choice
196
+TreeName = File path
197
+Content = Content
198
+
192 199
 require_error = ` cannot be empty.`
193 200
 alpha_dash_error = ` must be valid alpha or numeric or dash(-_) characters.`
194 201
 alpha_dash_dot_error = ` must be valid alpha or numeric or dash(-_) or dot characters.`
@@ -419,6 +426,51 @@ file_view_raw = View Raw
419 426
 file_permalink = Permalink
420 427
 file_too_large = This file is too large to be shown
421 428
 
429
+cancel = Cancel
430
+cancel_lower = cancel
431
+or = or
432
+new_file = New file
433
+upload_files = Upload files
434
+find_file = Find file
435
+commit_changes = Commit Changes
436
+default_commit_message = Add an optional extended description...
437
+last_commit_info = %s edited this file %s
438
+delete_this_file = Delete this file
439
+edit_this_file = Edit this file
440
+edit_file = Edit file
441
+delete_confirm_message = Are you sure you want to delete this file?
442
+delete_commit_message = Write a note about this delete (optional)
443
+file_editing_no_longer_exists = The file you are editing no longer exists in the repository
444
+file_already_exists = A file by that name already exists
445
+unable_to_update_file = Unable to update this file, error occurred
446
+add = Add
447
+update = Update
448
+filename_cannot_be_empty = Filename cannot be empty
449
+directory_is_a_file = One of the directories in the path is already a file in this repository
450
+filename_is_a_directory = The filename given is an existing directory in the repository
451
+must_be_on_branch = You must be on a branch to make or propose changes to this file
452
+must_be_writer = You must have write access to make or propose changes to this file
453
+cannot_edit_binary_files = Cannot edit binary files
454
+filename_help = To add directory, just type it and press /. To remove a directory, go to the beginning of the field and press backspace.
455
+fork_before_edit = You must fork this before editing
456
+branch_already_exists = Branch already exists
457
+create_new_branch = Create a %s for this commit and start a pull request.
458
+new_branch = new branch
459
+commit_directly_to_this_branch = Commit directly to the %s branch.
460
+create_branch = Create branch
461
+from = from
462
+upload_file = Upload file
463
+add_files_to_dir = Add files to %s
464
+unable_to_upload_files = Unable to upload files, an error occurred.
465
+add_subdir = Add subdirectory...
466
+name_your_file = Name your file...
467
+user_has_committed_since_you_started_editing = %s has committed since you started editing.
468
+see_what_changed = See what changed.
469
+pressing_commit_again_will_overwrite_those_changes = Pressing '%s' again will overwrite those changes.
470
+copy_file_path_to_clipboard = Copy file path to clipboard
471
+preview_changes = Preview Changes
472
+no_changes_to_show = There are no changes to show.
473
+
422 474
 commits.commits = Commits
423 475
 commits.search = Search commits
424 476
 commits.find = Find

+ 37 - 0
models/error.go

@@ -417,6 +417,19 @@ func (err ErrInvalidTagName) Error() string {
417 417
 	return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
418 418
 }
419 419
 
420
+type ErrRepoFileAlreadyExist struct {
421
+	FileName string
422
+}
423
+
424
+func IsErrRepoFileAlreadyExist(err error) bool {
425
+	_, ok := err.(ErrRepoFileAlreadyExist)
426
+	return ok
427
+}
428
+
429
+func (err ErrRepoFileAlreadyExist) Error() string {
430
+	return fmt.Sprintf("repository file already exists [file name: %s]", err.FileName)
431
+}
432
+
420 433
 // __________                             .__
421 434
 // \______   \____________    ____   ____ |  |__
422 435
 //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
@@ -628,3 +641,27 @@ func IsErrTeamAlreadyExist(err error) bool {
628 641
 func (err ErrTeamAlreadyExist) Error() string {
629 642
 	return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name)
630 643
 }
644
+
645
+//  ____ ___        .__                    .___
646
+// |    |   \______ |  |   _________     __| _/
647
+// |    |   /\____ \|  |  /  _ \__  \   / __ |
648
+// |    |  / |  |_> >  |_(  <_> ) __ \_/ /_/ |
649
+// |______/  |   __/|____/\____(____  /\____ |
650
+//           |__|                   \/      \/
651
+//
652
+
653
+type ErrUploadNotExist struct {
654
+	ID     int64
655
+	UUID   string
656
+	UserID int64
657
+	RepoID int64
658
+}
659
+
660
+func IsErrUploadNotExist(err error) bool {
661
+	_, ok := err.(ErrAttachmentNotExist)
662
+	return ok
663
+}
664
+
665
+func (err ErrUploadNotExist) Error() string {
666
+	return fmt.Sprintf("attachment does not exist [id: %d, uuid: %s]", err.ID, err.UUID)
667
+}

+ 5 - 10
models/issue.go

@@ -639,23 +639,18 @@ func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64,
639 639
 	}
640 640
 
641 641
 	// Check attachments.
642
-	attachments := make([]*Attachment, 0, len(uuids))
643 642
 	for _, uuid := range uuids {
644
-		attach, err := getAttachmentByUUID(e, uuid)
643
+		attachment, err := getAttachmentByUUID(e, uuid)
645 644
 		if err != nil {
646 645
 			if IsErrAttachmentNotExist(err) {
647 646
 				continue
648 647
 			}
649 648
 			return fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
650 649
 		}
651
-		attachments = append(attachments, attach)
652
-	}
653
-
654
-	for i := range attachments {
655
-		attachments[i].IssueID = issue.ID
650
+		attachment.IssueID = issue.ID
656 651
 		// No assign value could be 0, so ignore AllCols().
657
-		if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
658
-			return fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
652
+		if _, err = e.Id(attachment.ID).Update(attachment); err != nil {
653
+			return fmt.Errorf("update attachment[%d]: %v", attachment.ID, err)
659 654
 		}
660 655
 	}
661 656
 
@@ -1728,7 +1723,7 @@ func DeleteAttachments(attachments []*Attachment, remove bool) (int, error) {
1728 1723
 			}
1729 1724
 		}
1730 1725
 
1731
-		if _, err := x.Delete(a.ID); err != nil {
1726
+		if _, err := x.Delete(a); err != nil {
1732 1727
 			return i, err
1733 1728
 		}
1734 1729
 	}

+ 1 - 1
models/pull.go

@@ -354,7 +354,7 @@ func (pr *PullRequest) testPatch() (err error) {
354 354
 
355 355
 	log.Trace("PullRequest[%d].testPatch (patchPath): %s", pr.ID, patchPath)
356 356
 
357
-	if err := pr.BaseRepo.UpdateLocalCopy(); err != nil {
357
+	if err := pr.BaseRepo.UpdateLocalCopy(pr.BaseRepo.DefaultBranch); err != nil {
358 358
 		return fmt.Errorf("UpdateLocalCopy: %v", err)
359 359
 	}
360 360
 

+ 439 - 4
models/repo.go

@@ -9,7 +9,9 @@ import (
9 9
 	"errors"
10 10
 	"fmt"
11 11
 	"html/template"
12
+	"io"
12 13
 	"io/ioutil"
14
+	"mime/multipart"
13 15
 	"os"
14 16
 	"os/exec"
15 17
 	"path"
@@ -28,6 +30,7 @@ import (
28 30
 
29 31
 	git "github.com/gogits/git-module"
30 32
 	api "github.com/gogits/go-gogs-client"
33
+	gouuid "github.com/satori/go.uuid"
31 34
 
32 35
 	"github.com/gogits/gogs/modules/bindata"
33 36
 	"github.com/gogits/gogs/modules/log"
@@ -435,16 +438,25 @@ func (repo *Repository) LocalCopyPath() string {
435 438
 	return path.Join(setting.AppDataPath, "tmp/local", com.ToStr(repo.ID))
436 439
 }
437 440
 
438
-func updateLocalCopy(repoPath, localPath string) error {
441
+func updateLocalCopy(repoPath, localPath, branch string) error {
439 442
 	if !com.IsExist(localPath) {
440 443
 		if err := git.Clone(repoPath, localPath, git.CloneRepoOptions{
441 444
 			Timeout: time.Duration(setting.Git.Timeout.Clone) * time.Second,
445
+			Branch:  branch,
442 446
 		}); err != nil {
443 447
 			return fmt.Errorf("Clone: %v", err)
444 448
 		}
445 449
 	} else {
450
+		if err := git.Checkout(localPath, git.CheckoutOptions{
451
+			Branch:  branch,
452
+			Timeout: time.Duration(setting.Git.Timeout.Pull) * time.Second,
453
+		}); err != nil {
454
+			return fmt.Errorf("Checkout: %v", err)
455
+		}
446 456
 		if err := git.Pull(localPath, git.PullRemoteOptions{
447
-			All:     true,
457
+			All:     false,
458
+			Remote:  "origin",
459
+			Branch:  branch,
448 460
 			Timeout: time.Duration(setting.Git.Timeout.Pull) * time.Second,
449 461
 		}); err != nil {
450 462
 			return fmt.Errorf("Pull: %v", err)
@@ -454,8 +466,8 @@ func updateLocalCopy(repoPath, localPath string) error {
454 466
 }
455 467
 
456 468
 // UpdateLocalCopy makes sure the local copy of repository is up-to-date.
457
-func (repo *Repository) UpdateLocalCopy() error {
458
-	return updateLocalCopy(repo.RepoPath(), repo.LocalCopyPath())
469
+func (repo *Repository) UpdateLocalCopy(branch string) error {
470
+	return updateLocalCopy(repo.RepoPath(), repo.LocalCopyPath(), branch)
459 471
 }
460 472
 
461 473
 // PatchPath returns corresponding patch file path of repository by given issue ID.
@@ -2255,3 +2267,426 @@ func (repo *Repository) GetForks() ([]*Repository, error) {
2255 2267
 	forks := make([]*Repository, 0, repo.NumForks)
2256 2268
 	return forks, x.Find(&forks, &Repository{ForkID: repo.ID})
2257 2269
 }
2270
+
2271
+// ___________    .___.__  __    ___________.__.__
2272
+// \_   _____/  __| _/|__|/  |_  \_   _____/|__|  |   ____
2273
+//  |    __)_  / __ | |  \   __\  |    __)  |  |  | _/ __ \
2274
+//  |        \/ /_/ | |  ||  |    |     \   |  |  |_\  ___/
2275
+// /_______  /\____ | |__||__|    \___  /   |__|____/\___  >
2276
+//         \/      \/                 \/                 \/
2277
+
2278
+var repoWorkingPool = &workingPool{
2279
+	pool:  make(map[string]*sync.Mutex),
2280
+	count: make(map[string]int),
2281
+}
2282
+
2283
+func (repo *Repository) LocalRepoPath() string {
2284
+	return path.Join(setting.AppDataPath, "tmp/local-repo", com.ToStr(repo.ID))
2285
+}
2286
+
2287
+// UpdateLocalRepo makes sure the local copy of repository is up-to-date.
2288
+func (repo *Repository) UpdateLocalRepo(branchName string) error {
2289
+	return updateLocalCopy(repo.RepoPath(), repo.LocalRepoPath(), branchName)
2290
+}
2291
+
2292
+// DiscardLocalRepoChanges makes sure the local copy of repository is the same as the source
2293
+func (repo *Repository) DiscardLocalRepoChanges(branchName string) error {
2294
+	return discardLocalRepoChanges(repo.LocalRepoPath(), branchName)
2295
+}
2296
+
2297
+// discardLocalRepoChanges discards local commits make sure
2298
+// it is even to remote branch when local copy exists.
2299
+func discardLocalRepoChanges(localPath string, branch string) error {
2300
+	if !com.IsExist(localPath) {
2301
+		return nil
2302
+	}
2303
+	// No need to check if nothing in the repository.
2304
+	if !git.IsBranchExist(localPath, branch) {
2305
+		return nil
2306
+	}
2307
+	if err := git.ResetHEAD(localPath, true, "origin/"+branch); err != nil {
2308
+		return fmt.Errorf("ResetHEAD: %v", err)
2309
+	}
2310
+	return nil
2311
+}
2312
+
2313
+// CheckoutNewBranch checks out a new branch from the given branch name
2314
+func (repo *Repository) CheckoutNewBranch(oldBranchName, newBranchName string) error {
2315
+	return checkoutNewBranch(repo.RepoPath(), repo.LocalRepoPath(), oldBranchName, newBranchName)
2316
+}
2317
+
2318
+func checkoutNewBranch(repoPath, localPath, oldBranch, newBranch string) error {
2319
+	if !com.IsExist(localPath) {
2320
+		if err := updateLocalCopy(repoPath, localPath, oldBranch); err != nil {
2321
+			return err
2322
+		}
2323
+	}
2324
+	if err := git.Checkout(localPath, git.CheckoutOptions{
2325
+		Branch:    newBranch,
2326
+		OldBranch: oldBranch,
2327
+		Timeout:   time.Duration(setting.Git.Timeout.Pull) * time.Second,
2328
+	}); err != nil {
2329
+		return fmt.Errorf("Checkout New Branch: %v", err)
2330
+	}
2331
+	return nil
2332
+}
2333
+
2334
+// updateRepoFile adds new file to repository.
2335
+func (repo *Repository) UpdateRepoFile(doer *User, oldBranchName, branchName, oldTreeName, treeName, content, message string, isNewFile bool) (err error) {
2336
+	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
2337
+	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2338
+
2339
+	if err = repo.DiscardLocalRepoChanges(oldBranchName); err != nil {
2340
+		return fmt.Errorf("discardLocalRepoChanges: %s - %v", oldBranchName, err)
2341
+	} else if err = repo.UpdateLocalRepo(oldBranchName); err != nil {
2342
+		return fmt.Errorf("UpdateLocalRepo: %s - %v", oldBranchName, err)
2343
+	}
2344
+
2345
+	if oldBranchName != branchName {
2346
+		if err := repo.CheckoutNewBranch(oldBranchName, branchName); err != nil {
2347
+			return fmt.Errorf("CheckoutNewBranch: %s - %s: %v", oldBranchName, branchName, err)
2348
+		}
2349
+	}
2350
+
2351
+	localPath := repo.LocalRepoPath()
2352
+	filePath := path.Join(localPath, treeName)
2353
+
2354
+	if len(message) == 0 {
2355
+		if isNewFile {
2356
+			message = "Add '" + treeName + "'"
2357
+		} else {
2358
+			message = "Update '" + treeName + "'"
2359
+		}
2360
+	}
2361
+
2362
+	os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
2363
+
2364
+	// If new file, make sure it doesn't exist; if old file, move if file name change
2365
+	if isNewFile {
2366
+		if com.IsExist(filePath) {
2367
+			return ErrRepoFileAlreadyExist{filePath}
2368
+		}
2369
+	} else if oldTreeName != "" && treeName != "" && treeName != oldTreeName {
2370
+		if err = git.MoveFile(localPath, oldTreeName, treeName); err != nil {
2371
+			return fmt.Errorf("MoveFile: %v", err)
2372
+		}
2373
+	}
2374
+
2375
+	if err = ioutil.WriteFile(filePath, []byte(content), 0666); err != nil {
2376
+		return fmt.Errorf("WriteFile: %v", err)
2377
+	}
2378
+
2379
+	if err = git.AddChanges(localPath, true); err != nil {
2380
+		return fmt.Errorf("AddChanges: %v", err)
2381
+	} else if err = git.CommitChanges(localPath, message, doer.NewGitSig()); err != nil {
2382
+		return fmt.Errorf("CommitChanges: %v", err)
2383
+	} else if err = git.Push(localPath, "origin", branchName); err != nil {
2384
+		return fmt.Errorf("Push: %v", err)
2385
+	}
2386
+
2387
+	return nil
2388
+}
2389
+
2390
+func (repo *Repository) GetPreviewDiff(repoPath, branchName, treeName, text string, maxlines, maxchars, maxfiles int) (diff *Diff, err error) {
2391
+	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
2392
+	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2393
+
2394
+	if err = repo.DiscardLocalRepoChanges(branchName); err != nil {
2395
+		return nil, fmt.Errorf("discardLocalRepoChanges: %s - %v", branchName, err)
2396
+	} else if err = repo.UpdateLocalRepo(branchName); err != nil {
2397
+		return nil, fmt.Errorf("UpdateLocalRepo: %s - %v", branchName, err)
2398
+	}
2399
+
2400
+	localPath := repo.LocalRepoPath()
2401
+	filePath := path.Join(localPath, treeName)
2402
+
2403
+	os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
2404
+
2405
+	if err = ioutil.WriteFile(filePath, []byte(text), 0666); err != nil {
2406
+		return nil, fmt.Errorf("WriteFile: %v", err)
2407
+	}
2408
+
2409
+	var cmd *exec.Cmd
2410
+	cmd = exec.Command("git", "diff", treeName)
2411
+	cmd.Dir = localPath
2412
+	cmd.Stderr = os.Stderr
2413
+
2414
+	stdout, err := cmd.StdoutPipe()
2415
+	if err != nil {
2416
+		return nil, fmt.Errorf("StdoutPipe: %v", err)
2417
+	}
2418
+
2419
+	if err = cmd.Start(); err != nil {
2420
+		return nil, fmt.Errorf("Start: %v", err)
2421
+	}
2422
+
2423
+	pid := process.Add(fmt.Sprintf("GetDiffRange (%s)", repoPath), cmd)
2424
+	defer process.Remove(pid)
2425
+
2426
+	diff, err = ParsePatch(maxlines, maxchars, maxfiles, stdout)
2427
+	if err != nil {
2428
+		return nil, fmt.Errorf("ParsePatch: %v", err)
2429
+	}
2430
+
2431
+	if err = cmd.Wait(); err != nil {
2432
+		return nil, fmt.Errorf("Wait: %v", err)
2433
+	}
2434
+
2435
+	return diff, nil
2436
+}
2437
+
2438
+// ________         .__          __           ___________.__.__
2439
+// \______ \   ____ |  |   _____/  |_  ____   \_   _____/|__|  |   ____
2440
+//  |    |  \_/ __ \|  | _/ __ \   __\/ __ \   |    __)  |  |  | _/ __ \
2441
+//  |    `   \  ___/|  |_\  ___/|  | \  ___/   |     \   |  |  |_\  ___/
2442
+// /_______  /\___  >____/\___  >__|  \___  >  \___  /   |__|____/\___  >
2443
+//         \/     \/          \/          \/       \/                 \/
2444
+//
2445
+
2446
+func (repo *Repository) DeleteRepoFile(doer *User, branch, treeName, message string) (err error) {
2447
+	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
2448
+	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2449
+
2450
+	localPath := repo.LocalRepoPath()
2451
+	if err = discardLocalRepoChanges(localPath, branch); err != nil {
2452
+		return fmt.Errorf("discardLocalRepoChanges: %v", err)
2453
+	} else if err = repo.UpdateLocalRepo(branch); err != nil {
2454
+		return fmt.Errorf("UpdateLocalRepo: %v", err)
2455
+	}
2456
+
2457
+	filePath := path.Join(localPath, treeName)
2458
+	os.Remove(filePath)
2459
+
2460
+	if len(message) == 0 {
2461
+		message = "Delete file '" + treeName + "'"
2462
+	}
2463
+
2464
+	if err = git.AddChanges(localPath, true); err != nil {
2465
+		return fmt.Errorf("AddChanges: %v", err)
2466
+	} else if err = git.CommitChanges(localPath, message, doer.NewGitSig()); err != nil {
2467
+		return fmt.Errorf("CommitChanges: %v", err)
2468
+	} else if err = git.Push(localPath, "origin", branch); err != nil {
2469
+		return fmt.Errorf("Push: %v", err)
2470
+	}
2471
+
2472
+	return nil
2473
+}
2474
+
2475
+//  ____ ___        .__                    .___ ___________.___.__
2476
+// |    |   \______ |  |   _________     __| _/ \_   _____/|   |  |   ____   ______
2477
+// |    |   /\____ \|  |  /  _ \__  \   / __ |   |    __)  |   |  | _/ __ \ /  ___/
2478
+// |    |  / |  |_> >  |_(  <_> ) __ \_/ /_/ |   |     \   |   |  |_\  ___/ \___ \
2479
+// |______/  |   __/|____/\____(____  /\____ |   \___  /   |___|____/\___  >____  >
2480
+//           |__|                   \/      \/       \/                  \/     \/
2481
+//
2482
+
2483
+// uploadRepoFiles uploads new files to repository.
2484
+func (repo *Repository) UploadRepoFiles(doer *User, oldBranchName, branchName, treeName, message string, uuids []string) (err error) {
2485
+	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
2486
+	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2487
+
2488
+	localPath := repo.LocalRepoPath()
2489
+
2490
+	if err = discardLocalRepoChanges(localPath, oldBranchName); err != nil {
2491
+		return fmt.Errorf("discardLocalRepoChanges: %v", err)
2492
+	} else if err = repo.UpdateLocalRepo(oldBranchName); err != nil {
2493
+		return fmt.Errorf("UpdateLocalRepo: %v", err)
2494
+	}
2495
+
2496
+	if oldBranchName != branchName {
2497
+		repo.CheckoutNewBranch(oldBranchName, branchName)
2498
+	}
2499
+
2500
+	dirPath := path.Join(localPath, treeName)
2501
+	os.MkdirAll(dirPath, os.ModePerm)
2502
+
2503
+	// Copy uploaded files into repository.
2504
+	for _, uuid := range uuids {
2505
+		upload, err := getUpload(uuid, doer.ID, repo.ID)
2506
+		if err != nil {
2507
+			if IsErrUploadNotExist(err) {
2508
+				continue
2509
+			}
2510
+			return fmt.Errorf("getUpload[%s]: %v", uuid, err)
2511
+		}
2512
+		uuidPath := upload.LocalPath()
2513
+		filePath := dirPath + "/" + upload.Name
2514
+		if err := os.Rename(uuidPath, filePath); err != nil {
2515
+			DeleteUpload(upload, true)
2516
+			return fmt.Errorf("Rename[%s -> %s]: %v", uuidPath, filePath, err)
2517
+		}
2518
+		DeleteUpload(upload, false) // false because we have moved the file
2519
+	}
2520
+
2521
+	if len(message) == 0 {
2522
+		message = "Add files to '" + treeName + "'"
2523
+	}
2524
+
2525
+	if err = git.AddChanges(localPath, true); err != nil {
2526
+		return fmt.Errorf("AddChanges: %v", err)
2527
+	} else if err = git.CommitChanges(localPath, message, doer.NewGitSig()); err != nil {
2528
+		return fmt.Errorf("CommitChanges: %v", err)
2529
+	} else if err = git.Push(localPath, "origin", branchName); err != nil {
2530
+		return fmt.Errorf("Push: %v", err)
2531
+	}
2532
+
2533
+	return nil
2534
+}
2535
+
2536
+// Upload represent a uploaded file to a repo to be deleted when moved
2537
+type Upload struct {
2538
+	ID          int64  `xorm:"pk autoincr"`
2539
+	UUID        string `xorm:"uuid UNIQUE"`
2540
+	UID         int64  `xorm:"INDEX"`
2541
+	RepoID      int64  `xorm:"INDEX"`
2542
+	Name        string
2543
+	Created     time.Time `xorm:"-"`
2544
+	CreatedUnix int64
2545
+}
2546
+
2547
+func (u *Upload) BeforeInsert() {
2548
+	u.CreatedUnix = time.Now().UTC().Unix()
2549
+}
2550
+
2551
+func (u *Upload) AfterSet(colName string, _ xorm.Cell) {
2552
+	switch colName {
2553
+	case "created_unix":
2554
+		u.Created = time.Unix(u.CreatedUnix, 0).Local()
2555
+	}
2556
+}
2557
+
2558
+// UploadLocalPath returns where uploads is stored in local file system based on given UUID.
2559
+func UploadLocalPath(uuid string) string {
2560
+	return path.Join(setting.UploadTempPath, uuid[0:1], uuid[1:2], uuid)
2561
+}
2562
+
2563
+// LocalPath returns where uploads are temporarily stored in local file system.
2564
+func (upload *Upload) LocalPath() string {
2565
+	return UploadLocalPath(upload.UUID)
2566
+}
2567
+
2568
+// NewUpload creates a new upload object.
2569
+func NewUpload(name string, buf []byte, file multipart.File, userId, repoId int64) (_ *Upload, err error) {
2570
+	up := &Upload{
2571
+		UUID:   gouuid.NewV4().String(),
2572
+		Name:   name,
2573
+		UID:    userId,
2574
+		RepoID: repoId,
2575
+	}
2576
+
2577
+	if err = os.MkdirAll(path.Dir(up.LocalPath()), os.ModePerm); err != nil {
2578
+		return nil, fmt.Errorf("MkdirAll: %v", err)
2579
+	}
2580
+
2581
+	fw, err := os.Create(up.LocalPath())
2582
+	if err != nil {
2583
+		return nil, fmt.Errorf("Create: %v", err)
2584
+	}
2585
+	defer fw.Close()
2586
+
2587
+	if _, err = fw.Write(buf); err != nil {
2588
+		return nil, fmt.Errorf("Write: %v", err)
2589
+	} else if _, err = io.Copy(fw, file); err != nil {
2590
+		return nil, fmt.Errorf("Copy: %v", err)
2591
+	}
2592
+
2593
+	sess := x.NewSession()
2594
+	defer sessionRelease(sess)
2595
+	if err := sess.Begin(); err != nil {
2596
+		return nil, err
2597
+	}
2598
+	if _, err := sess.Insert(up); err != nil {
2599
+		return nil, err
2600
+	}
2601
+
2602
+	return up, sess.Commit()
2603
+}
2604
+
2605
+// RemoveUpload removes the file by UUID
2606
+func RemoveUpload(uuid string, userId, repoId int64) (err error) {
2607
+	sess := x.NewSession()
2608
+	defer sessionRelease(sess)
2609
+	if err := sess.Begin(); err != nil {
2610
+		return err
2611
+	}
2612
+	upload, err := getUpload(uuid, userId, repoId)
2613
+	if err != nil {
2614
+		return fmt.Errorf("getUpload[%s]: %v", uuid, err)
2615
+	}
2616
+
2617
+	if err := DeleteUpload(upload, true); err != nil {
2618
+		return fmt.Errorf("DeleteUpload[%s]: %v", uuid, err)
2619
+	}
2620
+
2621
+	return nil
2622
+}
2623
+
2624
+func getUpload(uuid string, userID, repoID int64) (*Upload, error) {
2625
+	up := &Upload{UUID: uuid, UID: userID, RepoID: repoID}
2626
+	has, err := x.Get(up)
2627
+	if err != nil {
2628
+		return nil, err
2629
+	} else if !has {
2630
+		return nil, ErrUploadNotExist{0, uuid, userID, repoID}
2631
+	}
2632
+	return up, nil
2633
+}
2634
+
2635
+// GetUpload returns Upload by given UUID.
2636
+func GetUpload(uuid string, userId, repoId int64) (*Upload, error) {
2637
+	return getUpload(uuid, userId, repoId)
2638
+}
2639
+
2640
+// DeleteUpload deletes the given upload
2641
+func DeleteUpload(u *Upload, remove bool) error {
2642
+	_, err := DeleteUploads([]*Upload{u}, remove)
2643
+	return err
2644
+}
2645
+
2646
+// DeleteUploads deletes the given uploads
2647
+func DeleteUploads(uploads []*Upload, remove bool) (int, error) {
2648
+	for i, u := range uploads {
2649
+		if remove {
2650
+			if err := os.Remove(u.LocalPath()); err != nil {
2651
+				return i, err
2652
+			}
2653
+		}
2654
+
2655
+		if _, err := x.Delete(u); err != nil {
2656
+			return i, err
2657
+		}
2658
+	}
2659
+
2660
+	return len(uploads), nil
2661
+}
2662
+
2663
+// __________                             .__
2664
+// \______   \____________    ____   ____ |  |__
2665
+//  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
2666
+//  |    |   \ |  | \// __ \|   |  \  \___|   Y  \
2667
+//  |______  / |__|  (____  /___|  /\___  >___|  /
2668
+//         \/             \/     \/     \/     \/
2669
+//
2670
+
2671
+func (repo *Repository) CreateNewBranch(doer *User, oldBranchName, branchName string) (err error) {
2672
+	repoWorkingPool.CheckIn(com.ToStr(repo.ID))
2673
+	defer repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2674
+
2675
+	localPath := repo.LocalRepoPath()
2676
+
2677
+	if err = discardLocalRepoChanges(localPath, oldBranchName); err != nil {
2678
+		return fmt.Errorf("discardLocalRepoChanges: %v", err)
2679
+	} else if err = repo.UpdateLocalRepo(oldBranchName); err != nil {
2680
+		return fmt.Errorf("UpdateLocalRepo: %v", err)
2681
+	}
2682
+
2683
+	if err = repo.CheckoutNewBranch(oldBranchName, branchName); err != nil {
2684
+		return fmt.Errorf("CreateNewBranch: %v", err)
2685
+	}
2686
+
2687
+	if err = git.Push(localPath, "origin", branchName); err != nil {
2688
+		return fmt.Errorf("Push: %v", err)
2689
+	}
2690
+
2691
+	return nil
2692
+}

+ 1 - 39
models/wiki.go

@@ -21,44 +21,6 @@ import (
21 21
 	"github.com/gogits/gogs/modules/setting"
22 22
 )
23 23
 
24
-// workingPool represents a pool of working status which makes sure
25
-// that only one instance of same task is performing at a time.
26
-// However, different type of tasks can performing at the same time.
27
-type workingPool struct {
28
-	lock  sync.Mutex
29
-	pool  map[string]*sync.Mutex
30
-	count map[string]int
31
-}
32
-
33
-// CheckIn checks in a task and waits if others are running.
34
-func (p *workingPool) CheckIn(name string) {
35
-	p.lock.Lock()
36
-
37
-	lock, has := p.pool[name]
38
-	if !has {
39
-		lock = &sync.Mutex{}
40
-		p.pool[name] = lock
41
-	}
42
-	p.count[name]++
43
-
44
-	p.lock.Unlock()
45
-	lock.Lock()
46
-}
47
-
48
-// CheckOut checks out a task to let other tasks run.
49
-func (p *workingPool) CheckOut(name string) {
50
-	p.lock.Lock()
51
-	defer p.lock.Unlock()
52
-
53
-	p.pool[name].Unlock()
54
-	if p.count[name] == 1 {
55
-		delete(p.pool, name)
56
-		delete(p.count, name)
57
-	} else {
58
-		p.count[name]--
59
-	}
60
-}
61
-
62 24
 var wikiWorkingPool = &workingPool{
63 25
 	pool:  make(map[string]*sync.Mutex),
64 26
 	count: make(map[string]int),
@@ -117,7 +79,7 @@ func (repo *Repository) LocalWikiPath() string {
117 79
 
118 80
 // UpdateLocalWiki makes sure the local copy of repository wiki is up-to-date.
119 81
 func (repo *Repository) UpdateLocalWiki() error {
120
-	return updateLocalCopy(repo.WikiPath(), repo.LocalWikiPath())
82
+	return updateLocalCopy(repo.WikiPath(), repo.LocalWikiPath(), "")
121 83
 }
122 84
 
123 85
 // discardLocalWikiChanges discards local commits make sure

+ 47 - 0
models/working_pool.go

@@ -0,0 +1,47 @@
1
+// Copyright 2015 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package models
6
+
7
+import (
8
+	"sync"
9
+)
10
+
11
+// workingPool represents a pool of working status which makes sure
12
+// that only one instance of same task is performing at a time.
13
+// However, different type of tasks can performing at the same time.
14
+type workingPool struct {
15
+	lock  sync.Mutex
16
+	pool  map[string]*sync.Mutex
17
+	count map[string]int
18
+}
19
+
20
+// CheckIn checks in a task and waits if others are running.
21
+func (p *workingPool) CheckIn(name string) {
22
+	p.lock.Lock()
23
+
24
+	lock, has := p.pool[name]
25
+	if !has {
26
+		lock = &sync.Mutex{}
27
+		p.pool[name] = lock
28
+	}
29
+	p.count[name]++
30
+
31
+	p.lock.Unlock()
32
+	lock.Lock()
33
+}
34
+
35
+// CheckOut checks out a task to let other tasks run.
36
+func (p *workingPool) CheckOut(name string) {
37
+	p.lock.Lock()
38
+	defer p.lock.Unlock()
39
+
40
+	p.pool[name].Unlock()
41
+	if p.count[name] == 1 {
42
+		delete(p.pool, name)
43
+		delete(p.count, name)
44
+	} else {
45
+		p.count[name]--
46
+	}
47
+}

+ 92 - 4
modules/auth/repo_form.go

@@ -169,7 +169,7 @@ type CreateIssueForm struct {
169 169
 	MilestoneID int64
170 170
 	AssigneeID  int64
171 171
 	Content     string
172
-	Attachments []string
172
+	Files       []string
173 173
 }
174 174
 
175 175
 func (f *CreateIssueForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -177,9 +177,9 @@ func (f *CreateIssueForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
177 177
 }
178 178
 
179 179
 type CreateCommentForm struct {
180
-	Content     string
181
-	Status      string `binding:"OmitEmpty;In(reopen,close)"`
182
-	Attachments []string
180
+	Content string
181
+	Status  string `binding:"OmitEmpty;In(reopen,close)"`
182
+	Files   []string
183 183
 }
184 184
 
185 185
 func (f *CreateCommentForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
@@ -269,3 +269,91 @@ type NewWikiForm struct {
269 269
 func (f *NewWikiForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
270 270
 	return validate(errs, ctx.Data, f, ctx.Locale)
271 271
 }
272
+
273
+// ___________    .___.__  __
274
+// \_   _____/  __| _/|__|/  |_
275
+//  |    __)_  / __ | |  \   __\
276
+//  |        \/ /_/ | |  ||  |
277
+// /_______  /\____ | |__||__|
278
+//         \/      \/
279
+
280
+type EditRepoFileForm struct {
281
+	TreeName      string `binding:"Required;MaxSize(500)"`
282
+	Content       string `binding:"Required"`
283
+	CommitSummary string `binding:"MaxSize(100)`
284
+	CommitMessage string
285
+	CommitChoice  string `binding:"Required;MaxSize(50)"`
286
+	NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
287
+	LastCommit    string
288
+}
289
+
290
+func (f *EditRepoFileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
291
+	return validate(errs, ctx.Data, f, ctx.Locale)
292
+}
293
+
294
+type EditPreviewDiffForm struct {
295
+	Content string
296
+}
297
+
298
+func (f *EditPreviewDiffForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
299
+	return validate(errs, ctx.Data, f, ctx.Locale)
300
+}
301
+
302
+//  ____ ___        .__                    .___
303
+// |    |   \______ |  |   _________     __| _/
304
+// |    |   /\____ \|  |  /  _ \__  \   / __ |
305
+// |    |  / |  |_> >  |_(  <_> ) __ \_/ /_/ |
306
+// |______/  |   __/|____/\____(____  /\____ |
307
+//           |__|                   \/      \/
308
+//
309
+
310
+type UploadRepoFileForm struct {
311
+	TreeName      string `binding:MaxSize(500)"`
312
+	CommitSummary string `binding:"MaxSize(100)`
313
+	CommitMessage string
314
+	CommitChoice  string `binding:"Required;MaxSize(50)"`
315
+	NewBranchName string `binding:"AlphaDashDot;MaxSize(100)"`
316
+	Files         []string
317
+}
318
+
319
+func (f *UploadRepoFileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
320
+	return validate(errs, ctx.Data, f, ctx.Locale)
321
+}
322
+
323
+type RemoveUploadFileForm struct {
324
+	File string `binding:"Required;MaxSize(50)"`
325
+}
326
+
327
+func (f *RemoveUploadFileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
328
+	return validate(errs, ctx.Data, f, ctx.Locale)
329
+}
330
+
331
+// ________         .__          __
332
+// \______ \   ____ |  |   _____/  |_  ____
333
+// |    |  \_/ __ \|  | _/ __ \   __\/ __ \
334
+// |    `   \  ___/|  |_\  ___/|  | \  ___/
335
+// /_______  /\___  >____/\___  >__|  \___  >
336
+//         \/     \/          \/          \/
337
+
338
+type DeleteRepoFileForm struct {
339
+	CommitSummary string `binding:"MaxSize(100)`
340
+}
341
+
342
+func (f *DeleteRepoFileForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
343
+	return validate(errs, ctx.Data, f, ctx.Locale)
344
+}
345
+
346
+// __________                             .__
347
+// \______   \____________    ____   ____ |  |__
348
+//  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
349
+//  |    |   \ |  | \// __ \|   |  \  \___|   Y  \
350
+//  |______  / |__|  (____  /___|  /\___  >___|  /
351
+//         \/             \/     \/     \/     \/
352
+type NewBranchForm struct {
353
+	OldBranchName string `binding:"Required;MaxSize(100)"`
354
+	BranchName    string `binding:"Required;MaxSize(100)"`
355
+}
356
+
357
+func (f *NewBranchForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
358
+	return validate(errs, ctx.Data, f, ctx.Locale)
359
+}

File diff suppressed because it is too large
+ 4 - 4
modules/bindata/bindata.go


+ 5 - 4
modules/markdown/markdown.go

@@ -53,10 +53,11 @@ func isLink(link []byte) bool {
53 53
 // IsMarkdownFile reports whether name looks like a Markdown file
54 54
 // based on its extension.
55 55
 func IsMarkdownFile(name string) bool {
56
-	name = strings.ToLower(name)
57
-	switch filepath.Ext(name) {
58
-	case ".md", ".markdown", ".mdown", ".mkd":
59
-		return true
56
+	extension := strings.ToLower(filepath.Ext(name))
57
+	for _, ext := range setting.Markdown.MdFileExtensions {
58
+		if strings.ToLower(ext) == extension {
59
+			return true
60
+		}
60 61
 	}
61 62
 	return false
62 63
 }

+ 30 - 0
modules/setting/setting.go

@@ -118,6 +118,12 @@ var (
118 118
 	RepoRootPath string
119 119
 	ScriptType   string
120 120
 
121
+	// Repo editor settings
122
+	Editor struct {
123
+		LineWrapExtensions     []string
124
+		PreviewTabApis         []string
125
+	}
126
+
121 127
 	// UI settings
122 128
 	UI struct {
123 129
 		ExplorePagingNum   int
@@ -141,6 +147,7 @@ var (
141 147
 	Markdown struct {
142 148
 		EnableHardLineBreak bool
143 149
 		CustomURLSchemes    []string `ini:"CUSTOM_URL_SCHEMES"`
150
+		MdFileExtensions    []string
144 151
 	}
145 152
 
146 153
 	// Picture settings
@@ -162,6 +169,13 @@ var (
162 169
 	AttachmentMaxFiles     int
163 170
 	AttachmentEnabled      bool
164 171
 
172
+	// Repo Upload settings
173
+	UploadTempPath         string
174
+	UploadAllowedTypes 	string
175
+	UploadMaxSize      	int64
176
+	UploadMaxFiles     	int
177
+	UploadEnabled      	bool
178
+
165 179
 	// Time settings
166 180
 	TimeFormat string
167 181
 
@@ -482,6 +496,16 @@ func NewContext() {
482 496
 		log.Fatal(4, "Fail to map Repository settings: %v", err)
483 497
 	}
484 498
 
499
+	sec = Cfg.Section("upload")
500
+	UploadTempPath = sec.Key("UPLOAD_TEMP_PATH").MustString(path.Join(AppDataPath, "tmp/uploads"))
501
+	if !filepath.IsAbs(UploadTempPath) {
502
+		UploadTempPath = path.Join(workDir, UploadTempPath)
503
+	}
504
+	UploadAllowedTypes = strings.Replace(sec.Key("UPLOAD_ALLOWED_TYPES").MustString(""), "|", ",", -1)
505
+	UploadMaxSize = sec.Key("UPLOAD_FILE_MAX_SIZE").MustInt64(32)
506
+	UploadMaxFiles = sec.Key("UPLOAD_MAX_FILES").MustInt(10)
507
+	UploadEnabled = sec.Key("ENABLE_UPLOADS").MustBool(true)
508
+
485 509
 	sec = Cfg.Section("picture")
486 510
 	AvatarUploadPath = sec.Key("AVATAR_UPLOAD_PATH").MustString(path.Join(AppDataPath, "avatars"))
487 511
 	forcePathSeparator(AvatarUploadPath)
@@ -532,6 +556,8 @@ func NewContext() {
532 556
 		log.Fatal(4, "Fail to map API settings: %v", err)
533 557
 	} else if err = Cfg.Section("api").MapTo(&API); err != nil {
534 558
 		log.Fatal(4, "Fail to map API settings: %v", err)
559
+	} else if err = Cfg.Section("editor").MapTo(&Editor); err != nil {
560
+		log.Fatal(4, "Fail to map Editor settings: %v", err)
535 561
 	}
536 562
 
537 563
 	if Mirror.DefaultInterval <= 0 {
@@ -546,6 +572,10 @@ func NewContext() {
546 572
 	ShowFooterVersion = Cfg.Section("other").Key("SHOW_FOOTER_VERSION").MustBool()
547 573
 
548 574
 	HasRobotsTxt = com.IsFile(path.Join(CustomPath, "robots.txt"))
575
+
576
+	Markdown.MdFileExtensions = Cfg.Section("markdown").Key("MD_FILE_EXTENSIONS").Strings(",")
577
+	Editor.LineWrapExtensions = Cfg.Section("editor").Key("LINE_WRAP_EXTENSIONS").Strings(",")
578
+	Editor.PreviewTabApis = Cfg.Section("editor").Key("PREVIEW_TAB_APIS").Strings(",")
549 579
 }
550 580
 
551 581
 var Service struct {

+ 224 - 27
public/css/gogs.css

@@ -255,6 +255,9 @@ code.wrap {
255 255
 .ui.status.buttons .octicon {
256 256
   margin-right: 4px;
257 257
 }
258
+.ui.menu .item .octicon {
259
+  margin-right: 4px;
260
+}
258 261
 .ui.inline.delete-button {
259 262
   padding: 8px 15px;
260 263
   font-weight: normal;
@@ -1266,57 +1269,57 @@ footer .ui.language .menu {
1266 1269
 .repository.file.list #file-content .view-raw img {
1267 1270
   padding: 5px 5px 0 5px;
1268 1271
 }
1269
-.repository.file.list #file-content .code-view * {
1272
+#file-content .code-view * {
1270 1273
   font-size: 12px;
1271 1274
   font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
1272 1275
   line-height: 20px;
1273 1276
 }
1274
-.repository.file.list #file-content .code-view table {
1277
+#file-content .code-view table {
1275 1278
   width: 100%;
1276 1279
 }
1277
-.repository.file.list #file-content .code-view .lines-num {
1280
+#file-content .code-view .lines-num {
1278 1281
   vertical-align: top;
1279 1282
   text-align: right;
1280 1283
   color: #999;
1281 1284
   background: #f5f5f5;
1282 1285
   width: 1%;
1283 1286
 }
1284
-.repository.file.list #file-content .code-view .lines-num span {
1287
+#file-content .code-view .lines-num span {
1285 1288
   line-height: 20px;
1286 1289
   padding: 0 10px;
1287 1290
   cursor: pointer;
1288 1291
   display: block;
1289 1292
 }
1290
-.repository.file.list #file-content .code-view .lines-num,
1291
-.repository.file.list #file-content .code-view .lines-code {
1293
+#file-content .code-view .lines-num,
1294
+#file-content .code-view .lines-code {
1292 1295
   padding: 0;
1293 1296
 }
1294
-.repository.file.list #file-content .code-view .lines-num pre,
1295
-.repository.file.list #file-content .code-view .lines-code pre,
1296
-.repository.file.list #file-content .code-view .lines-num ol,
1297
-.repository.file.list #file-content .code-view .lines-code ol,
1298
-.repository.file.list #file-content .code-view .lines-num .hljs,
1299
-.repository.file.list #file-content .code-view .lines-code .hljs {
1297
+#file-content .code-view .lines-num pre,
1298
+#file-content .code-view .lines-code pre,
1299
+#file-content .code-view .lines-num ol,
1300
+#file-content .code-view .lines-code ol,
1301
+#file-content .code-view .lines-num .hljs,
1302
+#file-content .code-view .lines-code .hljs {
1300 1303
   background-color: white;
1301 1304
   margin: 0;
1302 1305
   padding: 0 !important;
1303 1306
 }
1304
-.repository.file.list #file-content .code-view .lines-num pre li,
1305
-.repository.file.list #file-content .code-view .lines-code pre li,
1306
-.repository.file.list #file-content .code-view .lines-num ol li,
1307
-.repository.file.list #file-content .code-view .lines-code ol li,
1308
-.repository.file.list #file-content .code-view .lines-num .hljs li,
1309
-.repository.file.list #file-content .code-view .lines-code .hljs li {
1307
+#file-content .code-view .lines-num pre li,
1308
+#file-content .code-view .lines-code pre li,
1309
+#file-content .code-view .lines-num ol li,
1310
+#file-content .code-view .lines-code ol li,
1311
+#file-content .code-view .lines-num .hljs li,
1312
+#file-content .code-view .lines-code .hljs li {
1310 1313
   padding-left: 5px;
1311 1314
   display: inline-block;
1312 1315
   width: 100%;
1313 1316
 }
1314
-.repository.file.list #file-content .code-view .lines-num pre li.active,
1315
-.repository.file.list #file-content .code-view .lines-code pre li.active,
1316
-.repository.file.list #file-content .code-view .lines-num ol li.active,
1317
-.repository.file.list #file-content .code-view .lines-code ol li.active,
1318
-.repository.file.list #file-content .code-view .lines-num .hljs li.active,
1319
-.repository.file.list #file-content .code-view .lines-code .hljs li.active {
1317
+#file-content .code-view .lines-num pre li.active,
1318
+#file-content .code-view .lines-code pre li.active,
1319
+#file-content .code-view .lines-num ol li.active,
1320
+#file-content .code-view .lines-code ol li.active,
1321
+#file-content .code-view .lines-num .hljs li.active,
1322
+#file-content .code-view .lines-code .hljs li.active {
1320 1323
   background: #ffffdd;
1321 1324
 }
1322 1325
 .repository.file.list .sidebar {
@@ -1895,7 +1898,7 @@ footer .ui.language .menu {
1895 1898
   max-width: 100%;
1896 1899
   padding: 5px 5px 0 5px;
1897 1900
 }
1898
-.repository .code-view {
1901
+#file-content .code-view {
1899 1902
   overflow: auto;
1900 1903
   overflow-x: auto;
1901 1904
   overflow-y: hidden;
@@ -2157,13 +2160,13 @@ footer .ui.language .menu {
2157 2160
 .page.buttons {
2158 2161
   padding-top: 15px;
2159 2162
 }
2160
-.ui.comments .dropzone {
2163
+.ui.comments .dropzone, .ui.upload .dropzone {
2161 2164
   width: 100%;
2162 2165
   margin-bottom: 10px;
2163 2166
   border: 2px dashed #0087F7;
2164 2167
   box-shadow: none!important;
2165 2168
 }
2166
-.ui.comments .dropzone .dz-error-message {
2169
+.ui.comments .dropzone .dz-error-message, .ui.upload .dropzone .dz-error-message {
2167 2170
   top: 140px;
2168 2171
 }
2169 2172
 .settings .content {
@@ -2797,3 +2800,197 @@ footer .ui.language .menu {
2797 2800
 .ui.user.list .item .description a:hover {
2798 2801
   text-decoration: underline;
2799 2802
 }
2803
+.btn-octicon {
2804
+  display: inline-block;
2805
+  padding: 5px;
2806
+  margin-left: 5px;
2807
+  line-height: 1;
2808
+  color: #767676;
2809
+  vertical-align: middle;
2810
+  background: transparent;
2811
+  border: 0;
2812
+  outline: none;
2813
+}
2814
+.btn-octicon:hover {
2815
+  color: #4078c0;
2816
+}
2817
+.btn-octicon-danger:hover {
2818
+  color: #bd2c00;
2819
+}
2820
+.btn-octicon.disabled {
2821
+    color: #bbb;
2822
+    cursor: default;
2823
+}
2824
+.inline-form {
2825
+  display: inline-block;
2826
+}
2827
+.ui.form .breadcrumb input {
2828
+  min-height: 34px;
2829
+  padding: 7px 8px;
2830
+  color: #333;
2831
+  vertical-align: middle;
2832
+  background-color: #fff;
2833
+  background-repeat: no-repeat;
2834
+  background-position: right 8px center;
2835
+  border: 1px solid #ddd;
2836
+  border-radius: 3px;
2837
+  outline: none;
2838
+  box-shadow: inset 0 1px 2px rgba(0,0,0,0.075);
2839
+  width: inherit;
2840
+}
2841
+#file-actions {
2842
+  padding-left: 20px;
2843
+}
2844
+.CodeMirror.cm-s-default {
2845
+  margin-top: 20px;
2846
+  margin-bottom: 15px;
2847
+  border: 1px solid #ddd;
2848
+  border-radius: 3px;
2849
+  height: 600px;
2850
+  padding: 0 !important;
2851
+}
2852
+.commit-form-wrapper {
2853
+  padding-left: 64px;
2854
+}
2855
+.commit-form {
2856
+  position: relative;
2857
+  padding: 15px;
2858
+  margin-bottom: 10px;
2859
+  border: 1px solid #ddd;
2860
+  border-radius: 3px;
2861
+}
2862
+.commit-form-wrapper .commit-form-avatar {
2863
+  float: left;
2864
+  margin-left: -64px;
2865
+  border-radius: 4px;
2866
+}
2867
+.commit-form::before {
2868
+  border-width: 8px;
2869
+  border-color: transparent;
2870
+  border-right-color: #ddd;
2871
+  position: absolute;
2872
+  top: 11px;
2873
+  right: 100%;
2874
+  left: -16px;
2875
+  display: block;
2876
+  width: 0;
2877
+  height: 0;
2878
+  pointer-events: none;
2879
+  content: " ";
2880
+  border-style: solid solid outset;
2881
+}
2882
+.form-checkbox input[type=checkbox], .form-checkbox input[type=radio] {
2883
+  float: left;
2884
+  margin: 2px 0 0 -20px;
2885
+  vertical-align: middle;
2886
+  box-sizing: border-box;
2887
+  padding: 0;
2888
+}
2889
+.branch-name {
2890
+  display: inline-block;
2891
+  padding: 2px 6px;
2892
+  font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
2893
+  color: rgba(0,0,0,0.5);
2894
+  background-color: rgba(209,227,237,0.5);
2895
+  border-radius: 3px;
2896
+}
2897
+.form-control, .form-select {
2898
+  min-height: 34px;
2899
+  padding: 7px 8px;
2900
+  font-size: 13px;
2901
+  color: #333;
2902
+  vertical-align: middle;
2903
+  background-color: #fff;
2904
+  background-repeat: no-repeat;
2905
+  background-position: right 8px center;
2906
+  border: 1px solid #ddd;
2907
+  border-radius: 3px;
2908
+  outline: none;
2909
+  box-shadow: inset 0 1px 2px rgba(0,0,0,0.075);
2910
+}
2911
+.form-control.input-contrast {
2912
+  background-color: #fafafa;
2913
+}
2914
+.form-control.mr-2 {
2915
+  margin-right: 6px !important;
2916
+}
2917
+.quick-pull-choice .new-branch-name-input input {
2918
+  width: 240px !important;
2919
+  padding-left: 26px !important;
2920
+  font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
2921
+}
2922
+.quick-pull-choice .new-branch-name-input .quick-pull-new-branch-icon {
2923
+  position: absolute;
2924
+  top: 9px;
2925
+  left: 10px;
2926
+  color: #b0c4ce;
2927
+}
2928
+.text-muted, .text-gray {
2929
+  color: #767676 !important;
2930
+}
2931
+.quick-pull-choice .new-branch-name-input {
2932
+  position: relative;
2933
+  margin-top: 5px;
2934
+}
2935
+.quick-pull-choice .quick-pull-branch-name {
2936
+  display: none;
2937
+  padding-left: 48px;
2938
+  margin-top: 5px;
2939
+}
2940
+.quick-pull-choice.will-create-branch .quick-pull-branch-name {
2941
+  display: inline-block;
2942
+}
2943
+.nowrap {
2944
+  white-space: nowrap;
2945
+}
2946
+#file-buttons {
2947
+  padding-right: 15px;
2948
+}
2949
+.repository .ui.container .ui.breadcrumb {
2950
+  font-size: 1.5em;
2951
+  color: #767676;
2952
+  max-width: 600px;
2953
+}
2954
+.repository .ui.container .item:first-child .ui.breadcrumb {
2955
+  max-width: none;
2956
+}
2957
+.repository .ui.container .ui.breadcrumb.field {
2958
+  margin-bottom: 10px !important;
2959
+}
2960
+.repo-edit-file-cancel {
2961
+  padding-left: 10px;
2962
+}
2963
+#new-branch-item {
2964
+  display:none;
2965
+  margin: 0;
2966
+  text-align: left;
2967
+  padding: .71428571em 1.14285714em!important;
2968
+  background: 0 0!important;
2969
+  color: rgba(0,0,0,.87)!important;
2970
+  text-transform: none!important;
2971
+  box-shadow: none!important;
2972
+  -webkit-transition: none!important;
2973
+  transition: none!important;
2974
+  border-top: none;
2975
+  padding-right: calc(1.14285714rem + 17px)!important;
2976
+  font-size: 14px;
2977
+  font-weight: bold;
2978
+  line-height: 1.1;
2979
+}
2980
+#new-branch-item:hover {
2981
+  background: rgba(0,0,0,.05)!important;
2982
+  color: rgba(0,0,0,.95)!important;
2983
+}
2984
+#new-branch-item .icon {
2985
+  float: left;
2986
+  margin-left: -15px;
2987
+}
2988
+#new-branch-item .description {
2989
+  margin-top: 3px;
2990
+  font-size: 12px;
2991
+}
2992
+.repository .ui.container .ui.breadcrumb {
2993
+  font-size: 1.5em;
2994
+  color: #767676;
2995
+  max-width: 600px;
2996
+}

+ 423 - 81
public/js/gogs.js

@@ -28,6 +28,61 @@ function initCommentPreviewTab($form) {
28 28
     buttonsClickOnEnter();
29 29
 }
30 30
 
31
+var previewTab;
32
+var previewTabApis;
33
+
34
+function initEditPreviewTab($form) {
35
+    var $tab_menu = $form.find('.tabular.menu');
36
+    $tab_menu.find('.item').tab();
37
+    previewTab = $tab_menu.find('.item[data-tab="' + $tab_menu.data('preview') + '"]');
38
+
39
+    if (previewTab.length) {
40
+        previewTabApis = previewTab.data('preview-apis').split(',');
41
+        previewTab.click(function () {
42
+            var $this = $(this);
43
+            $.post($this.data('url'), {
44
+                    "_csrf": csrf,
45
+                    "mode": "gfm",
46
+                    "context": $this.data('context'),
47
+                    "text": $form.find('.tab.segment[data-tab="' + $tab_menu.data('write') + '"] textarea').val()
48
+                },
49
+                function (data) {
50
+                    var $preview_tab = $form.find('.tab.segment[data-tab="' + $tab_menu.data('preview') + '"]');
51
+                    $preview_tab.html(data);
52
+                    emojify.run($preview_tab[0]);
53
+                    $('pre code', $preview_tab[0]).each(function (i, block) {
54
+                        hljs.highlightBlock(block);
55
+                    });
56
+                }
57
+            );
58
+        });
59
+    }
60
+
61
+    buttonsClickOnEnter();
62
+}
63
+
64
+function initEditDiffTab($form) {
65
+    var $tab_menu = $form.find('.tabular.menu');
66
+    $tab_menu.find('.item').tab();
67
+    $tab_menu.find('.item[data-tab="' + $tab_menu.data('diff') + '"]').click(function () {
68
+        var $this = $(this);
69
+        $.post($this.data('url'), {
70
+                "_csrf": csrf,
71
+                "context": $this.data('context'),
72
+                "content": $form.find('.tab.segment[data-tab="' + $tab_menu.data('write') + '"] textarea').val()
73
+            },
74
+            function (data) {
75
+                var $diff_tab = $form.find('.tab.segment[data-tab="' + $tab_menu.data('diff') + '"]');
76
+                $diff_tab.html(data);
77
+                emojify.run($diff_tab[0]);
78
+                initCodeView()
79
+            }
80
+        );
81
+    });
82
+
83
+    buttonsClickOnEnter();
84
+}
85
+
31 86
 function initCommentForm() {
32 87
     if ($('.comment.form').length == 0) {
33 88
         return
@@ -145,6 +200,11 @@ function initCommentForm() {
145 200
     selectItem('.select-assignee', '#assignee_id');
146 201
 }
147 202
 
203
+function initEditForm() {
204
+    initEditPreviewTab($('.edit.form'));
205
+    initEditDiffTab($('.edit.form'));
206
+}
207
+
148 208
 function initInstall() {
149 209
     if ($('.install').length == 0) {
150 210
         return;
@@ -450,7 +510,7 @@ function initRepository() {
450 510
 
451 511
         // Change status
452 512
         var $status_btn = $('#status-button');
453
-        $('#content').keyup(function () {
513
+        $('#edit_area').keyup(function () {
454 514
             if ($(this).val().length == 0) {
455 515
                 $status_btn.text($status_btn.data('status'))
456 516
             } else {
@@ -516,15 +576,10 @@ function initRepositoryCollaboration() {
516 576
     });
517 577
 }
518 578
 
519
-function initWiki() {
520
-    if ($('.repository.wiki').length == 0) {
521
-        return;
522
-    }
523
-
524
-
525
-    if ($('.repository.wiki.new').length > 0) {
526
-        var $edit_area = $('#edit-area');
527
-        var simplemde = new SimpleMDE({
579
+function initWikiForm() {
580
+    var $edit_area = $('.repository.wiki textarea#edit_area');
581
+    if ($edit_area.length > 0) {
582
+        new SimpleMDE({
528 583
             autoDownloadFontAwesome: false,
529 584
             element: $edit_area[0],
530 585
             forceSync: true,
@@ -549,18 +604,284 @@ function initWiki() {
549 604
             renderingConfig: {
550 605
                 singleLineBreaks: false
551 606
             },
552
-            spellChecker: false,
607
+            indentWithTabs: false,
553 608
             tabSize: 4,
609
+            spellChecker: false,
554 610
             toolbar: ["bold", "italic", "strikethrough", "|",
555
-                "heading", "heading-1", "heading-2", "heading-3", "|",
611
+                "heading-1", "heading-2", "heading-3", "heading-bigger", "heading-smaller", "|",
556 612
                 "code", "quote", "|",
557 613
                 "unordered-list", "ordered-list", "|",
558
-                "link", "image", "horizontal-rule", "|",
559
-                "preview", "fullscreen"]
614
+                "link", "image", "table", "horizontal-rule", "|",
615
+                "clean-block", "preview", "fullscreen", "side-by-side"]
560 616
         })
561 617
     }
562 618
 }
563 619
 
620
+function initIssueForm() {
621
+    var $edit_area = $('.repository.issue textarea.edit_area');
622
+    if ($edit_area.length > 0) {
623
+        $edit_area.each(function (i, edit_area) {
624
+            new SimpleMDE({
625
+                autoDownloadFontAwesome: false,
626
+                element: edit_area[0],
627
+                forceSync: true,
628
+                previewRender: function (plainText, preview) { // Async method
629
+                    setTimeout(function () {
630
+                        // FIXME: still send render request when return back to edit mode
631
+                        $.post($edit_area.data('url'), {
632
+                                "_csrf": csrf,
633
+                                "mode": "gfm",
634
+                                "context": $edit_area.data('context'),
635
+                                "text": plainText
636
+                            },
637
+                            function (data) {
638
+                                preview.innerHTML = '<div class="markdown">' + data + '</div>';
639
+                                emojify.run($('.editor-preview')[0]);
640
+                            }
641
+                        );
642
+                    }, 0);
643
+
644
+                    return "Loading...";
645
+                },
646
+                renderingConfig: {
647
+                    singleLineBreaks: false
648
+                },
649
+                indentWithTabs: false,
650
+                tabSize: 4,
651
+                spellChecker: false,
652
+                toolbar: ["bold", "italic", "strikethrough", "|",
653
+                    "code", "quote", "|",
654
+                    "unordered-list", "ordered-list", "|",
655
+                    "link", "image", "table"]
656
+            })
657
+        });
658
+    }
659
+}
660
+
661
+var editArea;
662
+var editFilename;
663
+var smdEditor;
664
+var cmEditor;
665
+var mdFileExtensions;
666
+var lineWrapExtensions;
667
+
668
+// For IE
669
+String.prototype.endsWith = function (pattern) {
670
+    var d = this.length - pattern.length;
671
+    return d >= 0 && this.lastIndexOf(pattern) === d;
672
+};
673
+
674
+// Adding function to get the cursor position in a text field to jquery objects
675
+(function ($, undefined) {
676
+    $.fn.getCursorPosition = function () {
677
+        var el = $(this).get(0);
678
+        var pos = 0;
679
+        if ('selectionStart' in el) {
680
+            pos = el.selectionStart;
681
+        } else if ('selection' in document) {
682
+            el.focus();
683
+            var Sel = document.selection.createRange();
684
+            var SelLength = document.selection.createRange().text.length;
685
+            Sel.moveStart('character', -el.value.length);
686
+            pos = Sel.text.length - SelLength;
687
+        }
688
+        return pos;
689
+    }
690
+})(jQuery);
691
+
692
+function initEditor() {
693
+    editFilename = $("#file-name");
694
+    editFilename.keyup(function (e) {
695
+        var sections = $('.breadcrumb span.section');
696
+        var dividers = $('.breadcrumb div.divider');
697
+        if (e.keyCode == 8) {
698
+            if ($(this).getCursorPosition() == 0) {
699
+                if (sections.length > 0) {
700
+                    var value = sections.last().find('a').text();
701
+                    $(this).val(value + $(this).val());
702
+                    $(this)[0].setSelectionRange(value.length, value.length);
703
+                    sections.last().remove();
704
+                    dividers.last().remove();
705
+                }
706
+            }
707
+        }
708
+        if (e.keyCode == 191) {
709
+            var parts = $(this).val().split('/');
710
+            for (var i = 0; i < parts.length; ++i) {
711
+                var value = parts[i];
712
+                if (i < parts.length - 1) {
713
+                    if (value.length) {
714
+                        $('<span class="section"><a href="#">' + value + '</a></span>').insertBefore($(this));
715
+                        $('<div class="divider"> / </div>').insertBefore($(this));
716
+                    }
717
+                }
718
+                else {
719
+                    $(this).val(value);
720
+                }
721
+                $(this)[0].setSelectionRange(0, 0);
722
+            }
723
+        }
724
+        var parts = [];
725
+        $('.breadcrumb span.section').each(function (i, element) {
726
+            element = $(element);
727
+            if (element.find('a').length) {
728
+                parts.push(element.find('a').text());
729
+            } else {
730
+                parts.push(element.text());
731
+            }
732
+        });
733
+        if ($(this).val())
734
+            parts.push($(this).val());
735
+        $('#tree-name').val(parts.join('/'));
736
+    }).trigger('keyup');
737
+
738
+    editArea = $('.repository.edit textarea#edit_area');
739
+
740
+    if (!editArea.length)
741
+        return;
742
+
743
+    mdFileExtensions = editArea.data("md-file-extensions").split(",");
744
+    lineWrapExtensions = editArea.data("line-wrap-extensions").split(",");
745
+
746
+    editFilename.on("keyup", function (e) {
747
+        var val = editFilename.val(), m, mode, spec, extension, extWithDot, previewLink, dataUrl, apiCall;
748
+        extension = extWithDot = "";
749
+        if (m = /.+\.([^.]+)$/.exec(val)) {
750
+            extension = m[1];
751
+            extWithDot = "." + extension;
752
+        }
753
+
754
+        var info = CodeMirror.findModeByExtension(extension);
755
+        previewLink = $('a[data-tab=preview]');
756
+        if (info) {
757
+            mode = info.mode;
758
+            spec = info.mime;
759
+            apiCall = mode;
760
+        }
761
+        else {
762
+            apiCall = extension
763
+        }
764
+
765
+        if (previewLink.length && apiCall && previewTabApis && previewTabApis.length && previewTabApis.indexOf(apiCall) >= 0) {
766
+            dataUrl = previewLink.data('url');
767
+            previewLink.data('url', dataUrl.replace(/(.*)\/.*/i, '$1/' + mode));
768
+            previewLink.show();
769
+        }
770
+        else {
771
+            previewLink.hide();
772
+        }
773
+
774
+        // If this file is a Markdown extensions, we will load that editor and return
775
+        if (mdFileExtensions.indexOf(extWithDot) >= 0) {
776
+            if (setSimpleMDE()) {
777
+                return;
778
+            }
779
+        }
780
+
781
+        // Else we are going to use CodeMirror
782
+        if (!cmEditor) {
783
+            if (!setCodeMirror())
784
+                return;
785
+        }
786
+
787
+        if (mode) {
788
+            cmEditor.setOption("mode", spec);
789
+            CodeMirror.autoLoadMode(cmEditor, mode);
790
+        }
791
+
792
+        if (lineWrapExtensions.indexOf(extWithDot) >= 0) {
793
+            cmEditor.setOption("lineWrapping", true);
794
+        }
795
+        else {
796
+            cmEditor.setOption("lineWrapping", false);
797
+        }
798
+    }).trigger('keyup');
799
+}
800
+
801
+function setSimpleMDE() {
802
+    if (cmEditor) {
803
+        cmEditor.toTextArea();
804
+        cmEditor = null;
805
+    }
806
+
807
+    if (smdEditor) {
808
+        return true;
809
+    }
810
+
811
+    smdEditor = new SimpleMDE({
812
+        autoDownloadFontAwesome: false,
813
+        element: editArea[0],
814
+        forceSync: true,
815
+        renderingConfig: {
816
+            singleLineBreaks: false
817
+        },
818
+        indentWithTabs: false,
819
+        tabSize: 4,
820
+        spellChecker: false,
821
+        previewRender: function (plainText, preview) { // Async method
822
+            setTimeout(function () {
823
+                // FIXME: still send render request when return back to edit mode
824
+                $.post(editArea.data('url'), {
825
+                        "_csrf": csrf,
826
+                        "mode": "gfm",
827
+                        "context": editArea.data('context'),
828
+                        "text": plainText
829
+                    },
830
+                    function (data) {
831
+                        preview.innerHTML = '<div class="markdown">' + data + '</div>';
832
+                        emojify.run($('.editor-preview')[0]);
833
+                    }
834
+                );
835
+            }, 0);
836
+
837
+            return "Loading...";
838
+        },
839
+        toolbar: ["bold", "italic", "strikethrough", "|",
840
+            "heading-1", "heading-2", "heading-3", "heading-bigger", "heading-smaller", "|",
841
+            "code", "quote", "|",
842
+            "unordered-list", "ordered-list", "|",
843
+            "link", "image", "table", "horizontal-rule", "|",
844
+            "clean-block", "preview", "fullscreen", "side-by-side"]
845
+    });
846
+
847
+    return true;
848
+}
849
+
850
+function setCodeMirror() {
851
+    if (smdEditor) {
852
+        smdEditor.toTextArea();
853
+        smdEditor = null;
854
+    }
855
+
856
+    if (cmEditor) {
857
+        return true;
858
+    }
859
+
860
+    cmEditor = CodeMirror.fromTextArea(editArea[0], {
861
+        lineNumbers: true
862
+    });
863
+    cmEditor.on("change", function (cm, change) {
864
+        editArea.val(cm.getValue());
865
+    });
866
+
867
+    return true;
868
+}
869
+
870
+function initQuickPull() {
871
+    $('.js-quick-pull-choice-option').change(function () {
872
+        quickPullChoiceChange();
873
+    });
874
+    quickPullChoiceChange();
875
+}
876
+
877
+function quickPullChoiceChange() {
878
+    var radio = $('.js-quick-pull-choice-option:checked');
879
+    if (radio.val() == 'commit-to-new-branch')
880
+        $('.quick-pull-branch-name').show();
881
+    else
882
+        $('.quick-pull-branch-name').hide();
883
+}
884
+
564 885
 function initOrganization() {
565 886
     if ($('.organization').length == 0) {
566 887
         return;
@@ -867,6 +1188,37 @@ function searchRepositories() {
867 1188
     hideWhenLostFocus('#search-repo-box .results', '#search-repo-box');
868 1189
 }
869 1190
 
1191
+function initCodeView() {
1192
+    if ($('.code-view .linenums').length > 0) {
1193
+        $(document).on('click', '.lines-num span', function (e) {
1194
+            var $select = $(this);
1195
+            var $list = $select.parent().siblings('.lines-code').find('ol.linenums > li');
1196
+            selectRange($list, $list.filter('[rel=' + $select.attr('id') + ']'), (e.shiftKey ? $list.filter('.active').eq(0) : null));
1197
+            deSelect();
1198
+        });
1199
+
1200
+        $(window).on('hashchange', function (e) {
1201
+            var m = window.location.hash.match(/^#(L\d+)\-(L\d+)$/);
1202
+            var $list = $('.code-view ol.linenums > li');
1203
+            var $first;
1204
+            if (m) {
1205
+                $first = $list.filter('.' + m[1]);
1206
+                selectRange($list, $first, $list.filter('.' + m[2]));
1207
+                $("html, body").scrollTop($first.offset().top - 200);
1208
+                return;
1209
+            }
1210
+            m = window.location.hash.match(/^#(L\d+)$/);
1211
+            if (m) {
1212
+                $first = $list.filter('.' + m[1]);
1213
+                selectRange($list, $first);
1214
+                $("html, body").scrollTop($first.offset().top - 200);
1215
+            }
1216
+        }).trigger('hashchange');
1217
+    }
1218
+}
1219
+
1220
+var $dropz;
1221
+
870 1222
 $(document).ready(function () {
871 1223
     csrf = $('meta[name=_csrf]').attr("content");
872 1224
     suburl = $('meta[name=_suburl]').attr("content");
@@ -916,12 +1268,12 @@ $(document).ready(function () {
916 1268
     }
917 1269
 
918 1270
     // Dropzone
919
-    if ($('#dropzone').length > 0) {
1271
+    var $dropz = $('#dropzone');
1272
+    if ($dropz.length > 0) {
920 1273
         // Disable auto discover for all elements:
921 1274
         Dropzone.autoDiscover = false;
922 1275
 
923 1276
         var filenameDict = {};
924
-        var $dropz = $('#dropzone');
925 1277
         $dropz.dropzone({
926 1278
             url: $dropz.data('upload-url'),
927 1279
             headers: {"X-Csrf-Token": csrf},
@@ -936,12 +1288,16 @@ $(document).ready(function () {
936 1288
             init: function () {
937 1289
                 this.on("success", function (file, data) {
938 1290
                     filenameDict[file.name] = data.uuid;
939
-                    $('.attachments').append('<input id="' + data.uuid + '" name="attachments" type="hidden" value="' + data.uuid + '">');
1291
+                    var input = $('<input id="' + data.uuid + '" name="files" type="hidden">').val(data.uuid);
1292
+                    $('.files').append(input);
940 1293
                 });
941 1294
                 this.on("removedfile", function (file) {
942 1295
                     if (file.name in filenameDict) {
943 1296
                         $('#' + filenameDict[file.name]).remove();
944 1297
                     }
1298
+                    if ($dropz.data('remove-url') && $dropz.data('csrf')) {
1299
+                        $.post($dropz.data('remove-url'), {file: filenameDict[file.name], _csrf: $dropz.data('csrf')});
1300
+                    }
945 1301
                 })
946 1302
             }
947 1303
         });
@@ -975,6 +1331,15 @@ $(document).ready(function () {
975 1331
         e.trigger.setAttribute('data-content', e.trigger.getAttribute('data-original'))
976 1332
     });
977 1333
 
1334
+    // Clipboard for copying filename on edit page
1335
+    if ($('.clipboard-tree-name').length) {
1336
+        new Clipboard(document.querySelector('.clipboard-tree-name'), {
1337
+            text: function () {
1338
+                return $('#tree-name').val();
1339
+            }
1340
+        });
1341
+    }
1342
+
978 1343
     // Helpers.
979 1344
     $('.delete-button').click(function () {
980 1345
         var $this = $(this);
@@ -1038,10 +1403,14 @@ $(document).ready(function () {
1038 1403
     initCommentForm();
1039 1404
     initInstall();
1040 1405
     initRepository();
1041
-    initWiki();
1406
+    initWikiForm();
1407
+    initIssueForm();
1408
+    initEditForm();
1409
+    initEditor();
1042 1410
     initOrganization();
1043 1411
     initWebhook();
1044 1412
     initAdmin();
1413
+    initQuickPull();
1045 1414
 
1046 1415
     var routes = {
1047 1416
         'div.user.settings': initUserSettings,
@@ -1057,76 +1426,50 @@ $(document).ready(function () {
1057 1426
     }
1058 1427
 });
1059 1428
 
1060
-$(window).load(function () {
1061
-    function changeHash(hash) {
1062
-        if (history.pushState) {
1063
-            history.pushState(null, null, hash);
1064
-        }
1065
-        else {
1066
-            location.hash = hash;
1067
-        }
1429
+function changeHash(hash) {
1430
+    if (history.pushState) {
1431
+        history.pushState(null, null, hash);
1068 1432
     }
1069
-
1070
-    function deSelect() {
1071
-        if (window.getSelection) {
1072
-            window.getSelection().removeAllRanges();
1073
-        } else {
1074
-            document.selection.empty();
1075
-        }
1433
+    else {
1434
+        location.hash = hash;
1076 1435
     }
1436
+}
1077 1437
 
1078
-    function selectRange($list, $select, $from) {
1079
-        $list.removeClass('active');
1080
-        if ($from) {
1081
-            var a = parseInt($select.attr('rel').substr(1));
1082
-            var b = parseInt($from.attr('rel').substr(1));
1083
-            var c;
1084
-            if (a != b) {
1085
-                if (a > b) {
1086
-                    c = a;
1087
-                    a = b;
1088
-                    b = c;
1089
-                }
1090
-                var classes = [];
1091
-                for (var i = a; i <= b; i++) {
1092
-                    classes.push('.L' + i);
1093
-                }
1094
-                $list.filter(classes.join(',')).addClass('active');
1095
-                changeHash('#L' + a + '-' + 'L' + b);
1096
-                return
1097
-            }
1098
-        }
1099
-        $select.addClass('active');
1100
-        changeHash('#' + $select.attr('rel'));
1438
+function deSelect() {
1439
+    if (window.getSelection) {
1440
+        window.getSelection().removeAllRanges();
1441
+    } else {
1442
+        document.selection.empty();
1101 1443
     }
1444
+}
1102 1445
 
1103
-    // Code view.
1104
-    if ($('.code-view .linenums').length > 0) {
1105
-        $(document).on('click', '.lines-num span', function (e) {
1106
-            var $select = $(this);
1107
-            var $list = $select.parent().siblings('.lines-code').find('ol.linenums > li');
1108
-            selectRange($list, $list.filter('[rel=' + $select.attr('id') + ']'), (e.shiftKey ? $list.filter('.active').eq(0) : null));
1109
-            deSelect();
1110
-        });
1111
-
1112
-        $(window).on('hashchange', function (e) {
1113
-            var m = window.location.hash.match(/^#(L\d+)\-(L\d+)$/);
1114
-            var $list = $('.code-view ol.linenums > li');
1115
-            var $first;
1116
-            if (m) {
1117
-                $first = $list.filter('.' + m[1]);
1118
-                selectRange($list, $first, $list.filter('.' + m[2]));
1119
-                $("html, body").scrollTop($first.offset().top - 200);
1120
-                return;
1446
+function selectRange($list, $select, $from) {
1447
+    $list.removeClass('active');
1448
+    if ($from) {
1449
+        var a = parseInt($select.attr('rel').substr(1));
1450
+        var b = parseInt($from.attr('rel').substr(1));
1451
+        var c;
1452
+        if (a != b) {
1453
+            if (a > b) {
1454
+                c = a;
1455
+                a = b;
1456
+                b = c;
1121 1457
             }
1122
-            m = window.location.hash.match(/^#(L\d+)$/);
1123
-            if (m) {
1124
-                $first = $list.filter('.' + m[1]);
1125
-                selectRange($list, $first);
1126
-                $("html, body").scrollTop($first.offset().top - 200);
1458
+            var classes = [];
1459
+            for (var i = a; i <= b; i++) {
1460
+                classes.push('.L' + i);
1127 1461
             }
1128
-        }).trigger('hashchange');
1462
+            $list.filter(classes.join(',')).addClass('active');
1463
+            changeHash('#L' + a + '-' + 'L' + b);
1464
+            return
1465
+        }
1129 1466
     }
1467
+    $select.addClass('active');
1468
+    changeHash('#' + $select.attr('rel'));
1469
+}
1470
+
1471
+$(window).load(function () {
1472
+    initCodeView();
1130 1473
 
1131 1474
     // Repo clone url.
1132 1475
     if ($('#repo-clone-url').length > 0) {
@@ -1135,7 +1478,6 @@ $(window).load(function () {
1135 1478
                 if ($('#repo-clone-ssh').click().length === 0) {
1136 1479
                     $('#repo-clone-https').click();
1137 1480
                 }
1138
-                ;
1139 1481
                 break;
1140 1482
             default:
1141 1483
                 $('#repo-clone-https').click();

+ 55 - 0
routers/repo/branch.go

@@ -5,8 +5,13 @@
5 5
 package repo
6 6
 
7 7
 import (
8
+	"github.com/gogits/gogs/models"
9
+	"github.com/gogits/gogs/modules/auth"
8 10
 	"github.com/gogits/gogs/modules/base"
9 11
 	"github.com/gogits/gogs/modules/context"
12
+	"github.com/gogits/gogs/modules/log"
13
+	"net/url"
14
+	"strings"
10 15
 )
11 16
 
12 17
 const (
@@ -29,3 +34,53 @@ func Branches(ctx *context.Context) {
29 34
 	ctx.Data["Branches"] = brs
30 35
 	ctx.HTML(200, BRANCH)
31 36
 }
37
+
38
+func NewBranchPost(ctx *context.Context, form auth.NewBranchForm) {
39
+	oldBranchName := form.OldBranchName
40
+	branchName := form.BranchName
41
+
42
+	if ctx.HasError() || !ctx.Repo.IsWriter() || branchName == oldBranchName {
43
+		ctx.Redirect(EscapeUrl(ctx.Repo.RepoLink + "/src/" + oldBranchName))
44
+		return
45
+	}
46
+
47
+	branchName = url.QueryEscape(strings.Replace(strings.Trim(branchName, " "), " ", "-", -1))
48
+
49
+	if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
50
+		ctx.Redirect(EscapeUrl(ctx.Repo.RepoLink + "/src/" + branchName))
51
+		return
52
+	}
53
+
54
+	if err := ctx.Repo.Repository.CreateNewBranch(ctx.User, oldBranchName, branchName); err != nil {
55
+		ctx.Handle(404, "repo.Branches(CreateNewBranch)", err)
56
+		log.Error(4, "%s: %v", "EditFile", err)
57
+		return
58
+	}
59
+
60
+	// Was successful, so now need to call models.CommitRepoAction() with the new commitID for webhooks and watchers
61
+	if branch, err := ctx.Repo.Repository.GetBranch(branchName); err != nil {
62
+		log.Error(4, "repo.Repository.GetBranch(%s): %v", branchName, err)
63
+	} else if commit, err := branch.GetCommit(); err != nil {
64
+		log.Error(4, "branch.GetCommit(): %v", err)
65
+	} else {
66
+		pc := &models.PushCommits{
67
+			Len: 1,
68
+			Commits: []*models.PushCommit{&models.PushCommit{
69
+				commit.ID.String(),
70
+				commit.Message(),
71
+				commit.Author.Email,
72
+				commit.Author.Name,
73
+			}},
74
+		}
75
+		oldCommitID := "0000000000000000000000000000000000000000" // New Branch so we use all 0s
76
+		newCommitID := commit.ID.String()
77
+		if err := models.CommitRepoAction(ctx.User.ID, ctx.Repo.Owner.ID, ctx.User.LowerName, ctx.Repo.Owner.Email,
78
+			ctx.Repo.Repository.ID, ctx.Repo.Owner.LowerName, ctx.Repo.Repository.Name, "refs/heads/"+branchName, pc,
79
+			oldCommitID, newCommitID); err != nil {
80
+			log.Error(4, "models.CommitRepoAction(branch = %s): %v", branchName, err)
81
+		}
82
+		models.HookQueue.Add(ctx.Repo.Repository.ID)
83
+	}
84
+
85
+	ctx.Redirect(EscapeUrl(ctx.Repo.RepoLink + "/src/" + branchName))
86
+}

+ 54 - 0
routers/repo/delete.go

@@ -0,0 +1,54 @@
1
+// Copyright 2016 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package repo
6
+
7
+import (
8
+	"github.com/gogits/gogs/models"
9
+	"github.com/gogits/gogs/modules/auth"
10
+	"github.com/gogits/gogs/modules/context"
11
+	"github.com/gogits/gogs/modules/log"
12
+)
13
+
14
+func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
15
+	branchName := ctx.Repo.BranchName
16
+	treeName := ctx.Repo.TreeName
17
+
18
+	if ctx.HasError() {
19
+		ctx.Redirect(ctx.Repo.RepoLink + "/src/" + branchName + "/" + treeName)
20
+		return
21
+	}
22
+
23
+	if err := ctx.Repo.Repository.DeleteRepoFile(ctx.User, branchName, treeName, form.CommitSummary); err != nil {
24
+		ctx.Handle(500, "DeleteRepoFile", err)
25
+		return
26
+	}
27
+
28
+	// Was successful, so now need to call models.CommitRepoAction() with the new commitID for webhooks and watchers
29
+	if branch, err := ctx.Repo.Repository.GetBranch(branchName); err != nil {
30
+		log.Error(4, "repo.Repository.GetBranch(%s): %v", branchName, err)
31
+	} else if commit, err := branch.GetCommit(); err != nil {
32
+		log.Error(4, "branch.GetCommit(): %v", err)
33
+	} else {
34
+		pc := &models.PushCommits{
35
+			Len: 1,
36
+			Commits: []*models.PushCommit{&models.PushCommit{
37
+				commit.ID.String(),
38
+				commit.Message(),
39
+				commit.Author.Email,
40
+				commit.Author.Name,
41
+			}},
42
+		}
43
+		oldCommitID := ctx.Repo.CommitID
44
+		newCommitID := commit.ID.String()
45
+		if err := models.CommitRepoAction(ctx.User.ID, ctx.Repo.Owner.ID, ctx.User.LowerName, ctx.Repo.Owner.Email,
46
+			ctx.Repo.Repository.ID, ctx.Repo.Owner.LowerName, ctx.Repo.Repository.Name, "refs/heads/"+branchName, pc,
47
+			oldCommitID, newCommitID); err != nil {
48
+			log.Error(4, "models.CommitRepoAction(branch = %s): %v", branchName, err)
49
+		}
50
+		models.HookQueue.Add(ctx.Repo.Repository.ID)
51
+	}
52
+
53
+	ctx.Redirect(ctx.Repo.RepoLink + "/src/" + branchName)
54
+}

+ 359 - 0
routers/repo/edit.go

@@ -0,0 +1,359 @@
1
+// Copyright 2016 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package repo
6
+
7
+import (
8
+	"io/ioutil"
9
+	"path"
10
+	"strings"
11
+
12
+	"github.com/gogits/git-module"
13
+	"github.com/gogits/gogs/models"
14
+	"github.com/gogits/gogs/modules/auth"
15
+	"github.com/gogits/gogs/modules/base"
16
+	"github.com/gogits/gogs/modules/context"
17
+	"github.com/gogits/gogs/modules/log"
18
+	"github.com/gogits/gogs/modules/setting"
19
+	"github.com/gogits/gogs/modules/template"
20
+)
21
+
22
+const (
23
+	EDIT             base.TplName = "repo/edit"
24
+	DIFF_PREVIEW     base.TplName = "repo/diff_preview"
25
+	DIFF_PREVIEW_NEW base.TplName = "repo/diff_preview_new"
26
+)
27
+
28
+func EditFile(ctx *context.Context) {
29
+	editFile(ctx, false)
30
+}
31
+
32
+func NewFile(ctx *context.Context) {
33
+	editFile(ctx, true)
34
+}
35
+
36
+func editFile(ctx *context.Context, isNewFile bool) {
37
+	ctx.Data["PageIsEdit"] = true
38
+	ctx.Data["IsNewFile"] = isNewFile
39
+	ctx.Data["RequireHighlightJS"] = true
40
+
41
+	userName := ctx.Repo.Owner.Name
42
+	repoName := ctx.Repo.Repository.Name
43
+	branchName := ctx.Repo.BranchName
44
+	branchLink := ctx.Repo.RepoLink + "/src/" + branchName
45
+	treeName := ctx.Repo.TreeName
46
+
47
+	var treeNames []string
48
+	if len(treeName) > 0 {
49
+		treeNames = strings.Split(treeName, "/")
50
+	}
51
+
52
+	if !isNewFile {
53
+		entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treeName)
54
+
55
+		if err != nil && git.IsErrNotExist(err) {
56
+			ctx.Handle(404, "GetTreeEntryByPath", err)
57
+			return
58
+		}
59
+
60
+		if (ctx.Repo.IsViewCommit) || entry == nil || entry.IsDir() {
61
+			ctx.Handle(404, "repo.Home", nil)
62
+			return
63
+		}
64
+
65
+		blob := entry.Blob()
66
+
67
+		dataRc, err := blob.Data()
68
+		if err != nil {
69
+			ctx.Handle(404, "blob.Data", err)
70
+			return
71
+		}
72
+
73
+		ctx.Data["FileSize"] = blob.Size()
74
+		ctx.Data["FileName"] = blob.Name()
75
+
76
+		buf := make([]byte, 1024)
77
+		n, _ := dataRc.Read(buf)
78
+		if n > 0 {
79
+			buf = buf[:n]
80
+		}
81
+
82
+		_, isTextFile := base.IsTextFile(buf)
83
+
84
+		if !isTextFile {
85
+			ctx.Handle(404, "repo.Home", nil)
86
+			return
87
+		}
88
+
89
+		d, _ := ioutil.ReadAll(dataRc)
90
+		buf = append(buf, d...)
91
+
92
+		if err, content := template.ToUtf8WithErr(buf); err != nil {
93
+			if err != nil {
94
+				log.Error(4, "Convert content encoding: %s", err)
95
+			}
96
+			ctx.Data["FileContent"] = string(buf)
97
+		} else {
98
+			ctx.Data["FileContent"] = content
99
+		}
100
+	} else {
101
+		treeNames = append(treeNames, "")
102
+	}
103
+
104
+	ctx.Data["RequireSimpleMDE"] = true
105
+
106
+	ctx.Data["UserName"] = userName
107
+	ctx.Data["RepoName"] = repoName
108
+	ctx.Data["BranchName"] = branchName
109
+	ctx.Data["TreeName"] = treeName
110
+	ctx.Data["TreeNames"] = treeNames
111
+	ctx.Data["BranchLink"] = branchLink
112
+	ctx.Data["CommitSummary"] = ""
113
+	ctx.Data["CommitMessage"] = ""
114
+	ctx.Data["CommitChoice"] = "direct"
115
+	ctx.Data["NewBranchName"] = ""
116
+	ctx.Data["CommitDirectlyToThisBranch"] = ctx.Tr("repo.commit_directly_to_this_branch", "<strong class=\"branch-name\">"+branchName+"</strong>")
117
+	ctx.Data["CreateNewBranch"] = ctx.Tr("repo.create_new_branch", "<strong>"+ctx.Tr("repo.new_branch")+"</strong>")
118
+	ctx.Data["LastCommit"] = ctx.Repo.Commit.ID
119
+	ctx.Data["MdFileExtensions"] = strings.Join(setting.Markdown.MdFileExtensions, ",")
120
+	ctx.Data["LineWrapExtensions"] = strings.Join(setting.Editor.LineWrapExtensions, ",")
121
+	ctx.Data["PreviewTabApis"] = strings.Join(setting.Editor.PreviewTabApis, ",")
122
+	ctx.Data["PreviewDiffUrl"] = ctx.Repo.RepoLink + "/preview/" + branchName + "/" + treeName
123
+
124
+	ctx.HTML(200, EDIT)
125
+}
126
+
127
+func EditFilePost(ctx *context.Context, form auth.EditRepoFileForm) {
128
+	editFilePost(ctx, form, false)
129
+}
130
+
131
+func NewFilePost(ctx *context.Context, form auth.EditRepoFileForm) {
132
+	editFilePost(ctx, form, true)
133
+}
134
+
135
+func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bool) {
136
+	ctx.Data["PageIsEdit"] = true
137
+	ctx.Data["IsNewFile"] = isNewFile
138
+	ctx.Data["RequireHighlightJS"] = true
139
+
140
+	userName := ctx.Repo.Owner.Name
141
+	repoName := ctx.Repo.Repository.Name
142
+	oldBranchName := ctx.Repo.BranchName
143
+	branchName := oldBranchName
144
+	branchLink := ctx.Repo.RepoLink + "/src/" + branchName
145
+	oldTreeName := ctx.Repo.TreeName
146
+	content := form.Content
147
+	commitChoice := form.CommitChoice
148
+	lastCommit := form.LastCommit
149
+
150
+	if commitChoice == "commit-to-new-branch" {
151
+		branchName = form.NewBranchName
152
+	}
153
+
154
+	treeName := form.TreeName
155
+	treeName = strings.Trim(treeName, " ")
156
+	treeName = strings.Trim(treeName, "/")
157
+
158
+	var treeNames []string
159
+	if len(treeName) > 0 {
160
+		treeNames = strings.Split(treeName, "/")
161
+	}
162
+
163
+	ctx.Data["RequireSimpleMDE"] = true
164
+
165
+	ctx.Data["UserName"] = userName
166
+	ctx.Data["RepoName"] = repoName
167
+	ctx.Data["BranchName"] = branchName
168
+	ctx.Data["TreeName"] = treeName
169
+	ctx.Data["TreeNames"] = treeNames
170
+	ctx.Data["BranchLink"] = branchLink
171
+	ctx.Data["FileContent"] = content
172
+	ctx.Data["CommitSummary"] = form.CommitSummary
173
+	ctx.Data["CommitMessage"] = form.CommitMessage
174
+	ctx.Data["CommitChoice"] = commitChoice
175
+	ctx.Data["NewBranchName"] = branchName
176
+	ctx.Data["CommitDirectlyToThisBranch"] = ctx.Tr("repo.commit_directly_to_this_branch", "<strong class=\"branch-name\">"+oldBranchName+"</strong>")
177
+	ctx.Data["CreateNewBranch"] = ctx.Tr("repo.create_new_branch", "<strong>"+ctx.Tr("repo.new_branch")+"</strong>")
178
+	ctx.Data["LastCommit"] = ctx.Repo.Commit.ID
179
+	ctx.Data["MdFileExtensions"] = strings.Join(setting.Markdown.MdFileExtensions, ",")
180
+	ctx.Data["LineWrapExtensions"] = strings.Join(setting.Editor.LineWrapExtensions, ",")
181
+	ctx.Data["PreviewTabApis"] = strings.Join(setting.Editor.PreviewTabApis, ",")
182
+	ctx.Data["PreviewDiffUrl"] = ctx.Repo.RepoLink + "/preview/" + branchName + "/" + treeName
183
+
184
+	if ctx.HasError() {
185
+		ctx.HTML(200, EDIT)
186
+		return
187
+	}
188
+
189
+	if len(treeName) == 0 {
190
+		ctx.Data["Err_Filename"] = true
191
+		ctx.RenderWithErr(ctx.Tr("repo.filename_cannot_be_empty"), EDIT, &form)
192
+		log.Error(4, "%s: %s", "EditFile", "Filename can't be empty")
193
+		return
194
+	}
195
+
196
+	if oldBranchName != branchName {
197
+		if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
198
+			ctx.Data["Err_Branchname"] = true
199
+			ctx.RenderWithErr(ctx.Tr("repo.branch_already_exists"), EDIT, &form)
200
+			log.Error(4, "%s: %s - %s", "BranchName", branchName, "Branch already exists")
201
+			return
202
+		}
203
+
204
+	}
205
+
206
+	treepath := ""
207
+	for index, part := range treeNames {
208
+		treepath = path.Join(treepath, part)
209
+		entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treepath)
210
+		if err != nil {
211
+			// Means there is no item with that name, so we're good
212
+			break
213
+		}
214
+		if index != len(treeNames)-1 {
215
+			if !entry.IsDir() {
216
+				ctx.Data["Err_Filename"] = true
217
+				ctx.RenderWithErr(ctx.Tr("repo.directory_is_a_file"), EDIT, &form)
218
+				log.Error(4, "%s: %s - %s", "EditFile", treeName, "Directory given is a file")
219
+				return
220
+			}
221
+		} else {
222
+			if entry.IsDir() {
223
+				ctx.Data["Err_Filename"] = true
224
+				ctx.RenderWithErr(ctx.Tr("repo.filename_is_a_directory"), EDIT, &form)
225
+				log.Error(4, "%s: %s - %s", "EditFile", treeName, "Filename given is a dirctory")
226
+				return
227
+			}
228
+		}
229
+	}
230
+
231
+	if !isNewFile {
232
+		_, err := ctx.Repo.Commit.GetTreeEntryByPath(oldTreeName)
233
+		if err != nil && git.IsErrNotExist(err) {
234
+			ctx.Data["Err_Filename"] = true
235
+			ctx.RenderWithErr(ctx.Tr("repo.file_editing_no_longer_exists"), EDIT, &form)
236
+			log.Error(4, "%s: %s / %s - %s", "EditFile", branchName, oldTreeName, "File doesn't exist for editing")
237
+			return
238
+		}
239
+		if lastCommit != ctx.Repo.CommitID {
240
+			if files, err := ctx.Repo.Commit.GetFilesChangedSinceCommit(lastCommit); err == nil {
241
+				for _, file := range files {
242
+					if file == treeName {
243
+						name := ctx.Repo.Commit.Author.Name
244
+						if u, err := models.GetUserByEmail(ctx.Repo.Commit.Author.Email); err == nil {
245
+							name = `<a href="` + setting.AppSubUrl + "/" + u.Name + `" target="_blank">` + u.Name + `</a>`
246
+						}
247
+						message := ctx.Tr("repo.user_has_committed_since_you_started_editing", name) +
248
+							` <a href="` + ctx.Repo.RepoLink + "/commit/" + ctx.Repo.CommitID + `" target="_blank">` + ctx.Tr("repo.see_what_changed") + `</a>` +
249
+							" " + ctx.Tr("repo.pressing_commit_again_will_overwrite_those_changes", "<em>"+ctx.Tr("repo.commit_changes")+"</em>")
250
+						log.Error(4, "%s: %s / %s - %s", "EditFile", branchName, oldTreeName, "File updated by another user")
251
+						ctx.RenderWithErr(message, EDIT, &form)
252
+						return
253
+					}
254
+				}
255
+			}
256
+		}
257
+	}
258
+	if oldTreeName != treeName {
259
+		// We have a new filename (rename or completely new file) so we need to make sure it doesn't already exist, can't clobber
260
+		_, err := ctx.Repo.Commit.GetTreeEntryByPath(treeName)
261
+		if err == nil {
262
+			ctx.Data["Err_Filename"] = true
263
+			ctx.RenderWithErr(ctx.Tr("repo.file_already_exists"), EDIT, &form)
264
+			log.Error(4, "%s: %s - %s", "NewFile", treeName, "File already exists, can't create new")
265
+			return
266
+		}
267
+	}
268
+
269
+	message := ""
270
+	if form.CommitSummary != "" {
271
+		message = strings.Trim(form.CommitSummary, " ")
272
+	} else {
273
+		if isNewFile {
274
+			message = ctx.Tr("repo.add") + " '" + treeName + "'"
275
+		} else {
276
+			message = ctx.Tr("repo.update") + " '" + treeName + "'"
277
+		}
278
+	}
279
+	if strings.Trim(form.CommitMessage, " ") != "" {
280
+		message += "\n\n" + strings.Trim(form.CommitMessage, " ")
281
+	}
282
+
283
+	if err := ctx.Repo.Repository.UpdateRepoFile(ctx.User, oldBranchName, branchName, oldTreeName, treeName, content, message, isNewFile); err != nil {
284
+		ctx.Data["Err_Filename"] = true
285
+		ctx.RenderWithErr(ctx.Tr("repo.unable_to_update_file"), EDIT, &form)
286
+		log.Error(4, "%s: %v", "EditFile", err)
287
+		return
288
+	}
289
+
290
+	if branch, err := ctx.Repo.Repository.GetBranch(branchName); err != nil {
291
+		log.Error(4, "repo.Repository.GetBranch(%s): %v", branchName, err)
292
+	} else if commit, err := branch.GetCommit(); err != nil {
293
+		log.Error(4, "branch.GetCommit(): %v", err)
294
+	} else {
295
+		pc := &models.PushCommits{
296
+			Len: 1,
297
+			Commits: []*models.PushCommit{&models.PushCommit{
298
+				commit.ID.String(),
299
+				commit.Message(),
300
+				commit.Author.Email,
301
+				commit.Author.Name,
302
+			}},
303
+		}
304
+		oldCommitID := ctx.Repo.CommitID
305
+		newCommitID := commit.ID.String()
306
+		if branchName != oldBranchName {
307
+			oldCommitID = "0000000000000000000000000000000000000000" // New Branch so we use all 0s
308
+		}
309
+		if err := models.CommitRepoAction(ctx.User.ID, ctx.Repo.Owner.ID, ctx.User.LowerName, ctx.Repo.Owner.Email,
310
+			ctx.Repo.Repository.ID, ctx.Repo.Owner.LowerName, ctx.Repo.Repository.Name, "refs/heads/"+branchName, pc,
311
+			oldCommitID, newCommitID); err != nil {
312
+			log.Error(4, "models.CommitRepoAction(branch = %s): %v", branchName, err)
313
+		}
314
+		models.HookQueue.Add(ctx.Repo.Repository.ID)
315
+	}
316
+
317
+	// Leaving this off until forked repos that get a branch can compare with forks master and not upstream
318
+	//if oldBranchName != branchName {
319
+	//	ctx.Redirect(EscapeUrl(ctx.Repo.RepoLink + "/compare/" + oldBranchName + "..." + branchName))
320
+	//} else {
321
+	ctx.Redirect(EscapeUrl(ctx.Repo.RepoLink + "/src/" + branchName + "/" + treeName))
322
+	//}
323
+}
324
+
325
+func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) {
326
+	userName := ctx.Repo.Owner.Name
327
+	repoName := ctx.Repo.Repository.Name
328
+	branchName := ctx.Repo.BranchName
329
+	treeName := ctx.Repo.TreeName
330
+	content := form.Content
331
+
332
+	entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treeName)
333
+	if (err != nil && git.IsErrNotExist(err)) || entry.IsDir() {
334
+		ctx.Data["FileContent"] = content
335
+		ctx.HTML(200, DIFF_PREVIEW_NEW)
336
+		return
337
+	}
338
+
339
+	diff, err := ctx.Repo.Repository.GetPreviewDiff(models.RepoPath(userName, repoName), branchName, treeName, content, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles)
340
+	if err != nil {
341
+		ctx.Error(404, err.Error())
342
+		log.Error(4, "%s: %v", "GetPreviewDiff", err)
343
+		return
344
+	}
345
+
346
+	if diff.NumFiles() == 0 {
347
+		ctx.Error(200, ctx.Tr("repo.no_changes_to_show"))
348
+		return
349
+	}
350
+
351
+	ctx.Data["IsSplitStyle"] = ctx.Query("style") == "split"
352
+	ctx.Data["File"] = diff.Files[0]
353
+
354
+	ctx.HTML(200, DIFF_PREVIEW)
355
+}
356
+
357
+func EscapeUrl(str string) string {
358
+	return strings.NewReplacer("?", "%3F", "%", "%25", "#", "%23", " ", "%20", "^", "%5E", "\\", "%5C", "{", "%7B", "}", "%7D", "|", "%7C").Replace(str)
359
+}

+ 9 - 2
routers/repo/issue.go

@@ -340,6 +340,8 @@ func NewIssue(ctx *context.Context) {
340 340
 	}
341 341
 
342 342
 	ctx.Data["RequireHighlightJS"] = true
343
+	ctx.Data["RequireSimpleMDE"] = true
344
+	ctx.Data["RepoName"] = ctx.Repo.Repository.Name
343 345
 
344 346
 	ctx.HTML(200, ISSUE_NEW)
345 347
 }
@@ -401,6 +403,9 @@ func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm) ([]int64
401 403
 func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
402 404
 	ctx.Data["Title"] = ctx.Tr("repo.issues.new")
403 405
 	ctx.Data["PageIsIssueList"] = true
406
+	ctx.Data["RepoName"] = ctx.Repo.Repository.Name
407
+	ctx.Data["RequireHighlightJS"] = true
408
+	ctx.Data["RequireSimpleMDE"] = true
404 409
 	renderAttachmentSettings(ctx)
405 410
 
406 411
 	var (
@@ -414,7 +419,7 @@ func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
414 419
 	}
415 420
 
416 421
 	if setting.AttachmentEnabled {
417
-		attachments = form.Attachments
422
+		attachments = form.Files
418 423
 	}
419 424
 
420 425
 	if ctx.HasError() {
@@ -634,6 +639,8 @@ func ViewIssue(ctx *context.Context) {
634 639
 	ctx.Data["SignInLink"] = setting.AppSubUrl + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
635 640
 
636 641
 	ctx.Data["RequireHighlightJS"] = true
642
+	ctx.Data["RequireSimpleMDE"] = true
643
+	ctx.Data["RepoName"] = ctx.Repo.Repository.Name
637 644
 
638 645
 	ctx.HTML(200, ISSUE_VIEW)
639 646
 }
@@ -801,7 +808,7 @@ func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
801 808
 
802 809
 	var attachments []string
803 810
 	if setting.AttachmentEnabled {
804
-		attachments = form.Attachments
811
+		attachments = form.Files
805 812
 	}
806 813
 
807 814
 	if ctx.HasError() {

+ 1 - 1
routers/repo/pull.go

@@ -660,7 +660,7 @@ func CompareAndPullRequestPost(ctx *context.Context, form auth.CreateIssueForm)
660 660
 	}
661 661
 
662 662
 	if setting.AttachmentEnabled {
663
-		attachments = form.Attachments
663
+		attachments = form.Files
664 664
 	}
665 665
 
666 666
 	if ctx.HasError() {

+ 257 - 0
routers/repo/upload.go

@@ -0,0 +1,257 @@
1
+// Copyright 2016 The Gogs Authors. All rights reserved.
2
+// Use of this source code is governed by a MIT-style
3
+// license that can be found in the LICENSE file.
4
+
5
+package repo
6
+
7
+import (
8
+	"strings"
9
+
10
+	"fmt"
11
+	"github.com/gogits/gogs/models"
12
+	"github.com/gogits/gogs/modules/auth"
13
+	"github.com/gogits/gogs/modules/base"
14
+	"github.com/gogits/gogs/modules/context"
15
+	"github.com/gogits/gogs/modules/log"
16
+	"github.com/gogits/gogs/modules/setting"
17
+	"net/http"
18
+	"path"
19
+)
20
+
21
+const (
22
+	UPLOAD base.TplName = "repo/upload"
23
+)
24
+
25
+func renderUploadSettings(ctx *context.Context) {
26
+	ctx.Data["RequireDropzone"] = true
27
+	ctx.Data["IsUploadEnabled"] = setting.UploadEnabled
28
+	ctx.Data["UploadAllowedTypes"] = setting.UploadAllowedTypes
29
+	ctx.Data["UploadMaxSize"] = setting.UploadMaxSize
30
+	ctx.Data["UploadMaxFiles"] = setting.UploadMaxFiles
31
+}
32
+
33
+func UploadFile(ctx *context.Context) {
34
+	ctx.Data["PageIsUpload"] = true
35
+
36
+	userName := ctx.Repo.Owner.Name
37
+	repoName := ctx.Repo.Repository.Name
38
+	branchName := ctx.Repo.BranchName
39
+	branchLink := ctx.Repo.RepoLink + "/src/" + branchName
40
+	treeName := ctx.Repo.TreeName
41
+
42
+	treeNames := []string{""}
43
+	if len(treeName) > 0 {
44
+		treeNames = strings.Split(treeName, "/")
45
+	}
46
+
47
+	ctx.Data["UserName"] = userName
48
+	ctx.Data["RepoName"] = repoName
49
+	ctx.Data["BranchName"] = branchName
50
+	ctx.Data["TreeName"] = treeName
51
+	ctx.Data["TreeNames"] = treeNames
52
+	ctx.Data["BranchLink"] = branchLink
53
+	ctx.Data["CommitSummary"] = ""
54
+	ctx.Data["CommitMessage"] = ""
55
+	ctx.Data["CommitChoice"] = "direct"
56
+	ctx.Data["NewBranchName"] = ""
57
+	ctx.Data["CommitDirectlyToThisBranch"] = ctx.Tr("repo.commit_directly_to_this_branch", "<strong class=\"branch-name\">"+branchName+"</strong>")
58
+	ctx.Data["CreateNewBranch"] = ctx.Tr("repo.create_new_branch", "<strong>"+ctx.Tr("repo.new_branch")+"</strong>")
59
+	renderUploadSettings(ctx)
60
+
61
+	ctx.HTML(200, UPLOAD)
62
+}
63
+
64
+func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) {
65
+	ctx.Data["PageIsUpload"] = true
66
+	renderUploadSettings(ctx)
67
+
68
+	userName := ctx.Repo.Owner.Name
69
+	repoName := ctx.Repo.Repository.Name
70
+	oldBranchName := ctx.Repo.BranchName
71
+	branchName := oldBranchName
72
+	branchLink := ctx.Repo.RepoLink + "/src/" + branchName
73
+	commitChoice := form.CommitChoice
74
+	files := form.Files
75
+
76
+	if commitChoice == "commit-to-new-branch" {
77
+		branchName = form.NewBranchName
78
+	}
79
+
80
+	treeName := form.TreeName
81
+	treeName = strings.Trim(treeName, " ")
82
+	treeName = strings.Trim(treeName, "/")
83
+
84
+	treeNames := []string{""}
85
+	if len(treeName) > 0 {
86
+		treeNames = strings.Split(treeName, "/")
87
+	}
88
+
89
+	ctx.Data["UserName"] = userName
90
+	ctx.Data["RepoName"] = repoName
91
+	ctx.Data["BranchName"] = branchName
92
+	ctx.Data["TreeName"] = treeName
93
+	ctx.Data["TreeNames"] = treeNames
94
+	ctx.Data["BranchLink"] = branchLink
95
+	ctx.Data["CommitSummary"] = form.CommitSummary
96
+	ctx.Data["CommitMessage"] = form.CommitMessage
97
+	ctx.Data["CommitChoice"] = commitChoice
98
+	ctx.Data["NewBranchName"] = branchName
99
+	ctx.Data["CommitDirectlyToThisBranch"] = ctx.Tr("repo.commit_directly_to_this_branch", "<strong class=\"branch-name\">"+oldBranchName+"</strong>")
100
+	ctx.Data["CreateNewBranch"] = ctx.Tr("repo.create_new_branch", "<strong>"+ctx.Tr("repo.new_branch")+"</strong>")
101
+
102
+	if ctx.HasError() {
103
+		ctx.HTML(200, UPLOAD)
104
+		return
105
+	}
106
+
107
+	if oldBranchName != branchName {
108
+		if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
109
+			ctx.Data["Err_Branchname"] = true
110
+			ctx.RenderWithErr(ctx.Tr("repo.branch_already_exists"), UPLOAD, &form)
111
+			log.Error(4, "%s: %s - %s", "BranchName", branchName, "Branch already exists")
112
+			return
113
+		}
114
+
115
+	}
116
+
117
+	treepath := ""
118
+	for _, part := range treeNames {
119
+		treepath = path.Join(treepath, part)
120
+		entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treepath)
121
+		if err != nil {
122
+			// Means there is no item with that name, so we're good
123
+			break
124
+		}
125
+		if !entry.IsDir() {
126
+			ctx.Data["Err_Filename"] = true
127
+			ctx.RenderWithErr(ctx.Tr("repo.directory_is_a_file"), UPLOAD, &form)
128
+			log.Error(4, "%s: %s - %s", "UploadFile", treeName, "Directory given is a file")
129
+			return
130
+		}
131
+	}
132
+
133
+	message := ""
134
+	if form.CommitSummary != "" {
135
+		message = strings.Trim(form.CommitSummary, " ")
136
+	} else {
137
+		message = ctx.Tr("repo.add_files_to_dir", "'"+treeName+"'")
138
+	}
139
+	if strings.Trim(form.CommitMessage, " ") != "" {
140
+		message += "\n\n" + strings.Trim(form.CommitMessage, " ")
141
+	}
142
+
143
+	if err := ctx.Repo.Repository.UploadRepoFiles(ctx.User, oldBranchName, branchName, treeName, message, files); err != nil {
144
+		ctx.Data["Err_Directory"] = true
145
+		ctx.RenderWithErr(ctx.Tr("repo.unable_to_upload_files"), UPLOAD, &form)
146
+		log.Error(4, "%s: %v", "UploadFile", err)
147
+		return
148
+	}
149
+
150
+	// Was successful, so now need to call models.CommitRepoAction() with the new commitID for webhooks and watchers
151
+	if branch, err := ctx.Repo.Repository.GetBranch(branchName); err != nil {
152
+		log.Error(4, "repo.Repository.GetBranch(%s): %v", branchName, err)
153
+	} else if commit, err := branch.GetCommit(); err != nil {
154
+		log.Error(4, "branch.GetCommit(): %v", err)
155
+	} else {
</