Browse Source

Make URL scheme unambiguous (#2408)

* Make URL scheme unambiguous

Redirect old routes to new routes

* Fix redirects to new URL scheme, and update template

* Fix branches/_new endpoints, and update integration test
Ethan Koenig 1 year ago
parent
commit
513375c429

+ 2 - 2
integrations/editor_test.go

@@ -111,7 +111,7 @@ func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePa
111 111
 	resp = session.MakeRequest(t, req, http.StatusFound)
112 112
 
113 113
 	// Verify the change
114
-	req = NewRequest(t, "GET", path.Join(user, repo, "raw", branch, filePath))
114
+	req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath))
115 115
 	resp = session.MakeRequest(t, req, http.StatusOK)
116 116
 	assert.EqualValues(t, newContent, string(resp.Body))
117 117
 
@@ -142,7 +142,7 @@ func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, bra
142 142
 	resp = session.MakeRequest(t, req, http.StatusFound)
143 143
 
144 144
 	// Verify the change
145
-	req = NewRequest(t, "GET", path.Join(user, repo, "raw", targetBranch, filePath))
145
+	req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath))
146 146
 	resp = session.MakeRequest(t, req, http.StatusOK)
147 147
 	assert.EqualValues(t, newContent, string(resp.Body))
148 148
 

+ 17 - 1
integrations/links_test.go

@@ -10,6 +10,8 @@ import (
10 10
 	"testing"
11 11
 
12 12
 	api "code.gitea.io/sdk/gitea"
13
+
14
+	"github.com/stretchr/testify/assert"
13 15
 )
14 16
 
15 17
 func TestLinksNoLogin(t *testing.T) {
@@ -38,6 +40,20 @@ func TestLinksNoLogin(t *testing.T) {
38 40
 	}
39 41
 }
40 42
 
43
+func TestRedirectsNoLogin(t *testing.T) {
44
+	prepareTestEnv(t)
45
+
46
+	var redirects = map[string]string{
47
+		"/user2/repo1/commits/master": "/user2/repo1/commits/branch/master",
48
+		"/user2/repo1/src/master":     "/user2/repo1/src/branch/master",
49
+	}
50
+	for link, redirectLink := range redirects {
51
+		req := NewRequest(t, "GET", link)
52
+		resp := MakeRequest(t, req, http.StatusFound)
53
+		assert.EqualValues(t, redirectLink, RedirectURL(t, resp))
54
+	}
55
+}
56
+
41 57
 func testLinksAsUser(userName string, t *testing.T) {
42 58
 	var links = []string{
43 59
 		"/explore/repos",
@@ -99,7 +115,7 @@ func testLinksAsUser(userName string, t *testing.T) {
99 115
 		"",
100 116
 		"/issues",
101 117
 		"/pulls",
102
-		"/commits/master",
118
+		"/commits/branch/master",
103 119
 		"/graph",
104 120
 		"/settings",
105 121
 		"/settings/collaboration",

+ 52 - 52
integrations/repo_branch_test.go

@@ -14,14 +14,14 @@ import (
14 14
 	"github.com/stretchr/testify/assert"
15 15
 )
16 16
 
17
-func testCreateBranch(t *testing.T, session *TestSession, user, repo, oldRefName, newBranchName string, expectedStatus int) string {
17
+func testCreateBranch(t *testing.T, session *TestSession, user, repo, oldRefSubURL, newBranchName string, expectedStatus int) string {
18 18
 	var csrf string
19 19
 	if expectedStatus == http.StatusNotFound {
20
-		csrf = GetCSRF(t, session, path.Join(user, repo, "src/master"))
20
+		csrf = GetCSRF(t, session, path.Join(user, repo, "src/branch/master"))
21 21
 	} else {
22
-		csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefName))
22
+		csrf = GetCSRF(t, session, path.Join(user, repo, "src", oldRefSubURL))
23 23
 	}
24
-	req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefName), map[string]string{
24
+	req := NewRequestWithValues(t, "POST", path.Join(user, repo, "branches/_new", oldRefSubURL), map[string]string{
25 25
 		"_csrf":           csrf,
26 26
 		"new_branch_name": newBranchName,
27 27
 	})
@@ -34,72 +34,72 @@ func testCreateBranch(t *testing.T, session *TestSession, user, repo, oldRefName
34 34
 
35 35
 func TestCreateBranch(t *testing.T) {
36 36
 	tests := []struct {
37
-		OldBranchOrCommit string
38
-		NewBranch         string
39
-		CreateRelease     string
40
-		FlashMessage      string
41
-		ExpectedStatus    int
37
+		OldRefSubURL   string
38
+		NewBranch      string
39
+		CreateRelease  string
40
+		FlashMessage   string
41
+		ExpectedStatus int
42 42
 	}{
43 43
 		{
44
-			OldBranchOrCommit: "master",
45
-			NewBranch:         "feature/test1",
46
-			ExpectedStatus:    http.StatusFound,
47
-			FlashMessage:      i18n.Tr("en", "repo.branch.create_success", "feature/test1"),
44
+			OldRefSubURL:   "branch/master",
45
+			NewBranch:      "feature/test1",
46
+			ExpectedStatus: http.StatusFound,
47
+			FlashMessage:   i18n.Tr("en", "repo.branch.create_success", "feature/test1"),
48 48
 		},
49 49
 		{
50
-			OldBranchOrCommit: "master",
51
-			NewBranch:         "",
52
-			ExpectedStatus:    http.StatusFound,
53
-			FlashMessage:      i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.require_error"),
50
+			OldRefSubURL:   "branch/master",
51
+			NewBranch:      "",
52
+			ExpectedStatus: http.StatusFound,
53
+			FlashMessage:   i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.require_error"),
54 54
 		},
55 55
 		{
56
-			OldBranchOrCommit: "master",
57
-			NewBranch:         "feature=test1",
58
-			ExpectedStatus:    http.StatusFound,
59
-			FlashMessage:      i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.git_ref_name_error"),
56
+			OldRefSubURL:   "branch/master",
57
+			NewBranch:      "feature=test1",
58
+			ExpectedStatus: http.StatusFound,
59
+			FlashMessage:   i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.git_ref_name_error"),
60 60
 		},
61 61
 		{
62
-			OldBranchOrCommit: "master",
63
-			NewBranch:         strings.Repeat("b", 101),
64
-			ExpectedStatus:    http.StatusFound,
65
-			FlashMessage:      i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.max_size_error", "100"),
62
+			OldRefSubURL:   "branch/master",
63
+			NewBranch:      strings.Repeat("b", 101),
64
+			ExpectedStatus: http.StatusFound,
65
+			FlashMessage:   i18n.Tr("en", "form.NewBranchName") + i18n.Tr("en", "form.max_size_error", "100"),
66 66
 		},
67 67
 		{
68
-			OldBranchOrCommit: "master",
69
-			NewBranch:         "master",
70
-			ExpectedStatus:    http.StatusFound,
71
-			FlashMessage:      i18n.Tr("en", "repo.branch.branch_already_exists", "master"),
68
+			OldRefSubURL:   "branch/master",
69
+			NewBranch:      "master",
70
+			ExpectedStatus: http.StatusFound,
71
+			FlashMessage:   i18n.Tr("en", "repo.branch.branch_already_exists", "master"),
72 72
 		},
73 73
 		{
74
-			OldBranchOrCommit: "master",
75
-			NewBranch:         "master/test",
76
-			ExpectedStatus:    http.StatusFound,
77
-			FlashMessage:      i18n.Tr("en", "repo.branch.branch_name_conflict", "master/test", "master"),
74
+			OldRefSubURL:   "branch/master",
75
+			NewBranch:      "master/test",
76
+			ExpectedStatus: http.StatusFound,
77
+			FlashMessage:   i18n.Tr("en", "repo.branch.branch_name_conflict", "master/test", "master"),
78 78
 		},
79 79
 		{
80
-			OldBranchOrCommit: "acd1d892867872cb47f3993468605b8aa59aa2e0",
81
-			NewBranch:         "feature/test2",
82
-			ExpectedStatus:    http.StatusNotFound,
80
+			OldRefSubURL:   "commit/acd1d892867872cb47f3993468605b8aa59aa2e0",
81
+			NewBranch:      "feature/test2",
82
+			ExpectedStatus: http.StatusNotFound,
83 83
 		},
84 84
 		{
85
-			OldBranchOrCommit: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
86
-			NewBranch:         "feature/test3",
87
-			ExpectedStatus:    http.StatusFound,
88
-			FlashMessage:      i18n.Tr("en", "repo.branch.create_success", "feature/test3"),
85
+			OldRefSubURL:   "commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
86
+			NewBranch:      "feature/test3",
87
+			ExpectedStatus: http.StatusFound,
88
+			FlashMessage:   i18n.Tr("en", "repo.branch.create_success", "feature/test3"),
89 89
 		},
90 90
 		{
91
-			OldBranchOrCommit: "master",
92
-			NewBranch:         "v1.0.0",
93
-			CreateRelease:     "v1.0.0",
94
-			ExpectedStatus:    http.StatusFound,
95
-			FlashMessage:      i18n.Tr("en", "repo.branch.tag_collision", "v1.0.0"),
91
+			OldRefSubURL:   "branch/master",
92
+			NewBranch:      "v1.0.0",
93
+			CreateRelease:  "v1.0.0",
94
+			ExpectedStatus: http.StatusFound,
95
+			FlashMessage:   i18n.Tr("en", "repo.branch.tag_collision", "v1.0.0"),
96 96
 		},
97 97
 		{
98
-			OldBranchOrCommit: "v1.0.0",
99
-			NewBranch:         "feature/test4",
100
-			CreateRelease:     "v1.0.0",
101
-			ExpectedStatus:    http.StatusFound,
102
-			FlashMessage:      i18n.Tr("en", "repo.branch.create_success", "feature/test4"),
98
+			OldRefSubURL:   "tag/v1.0.0",
99
+			NewBranch:      "feature/test4",
100
+			CreateRelease:  "v1.0.0",
101
+			ExpectedStatus: http.StatusFound,
102
+			FlashMessage:   i18n.Tr("en", "repo.branch.create_success", "feature/test4"),
103 103
 		},
104 104
 	}
105 105
 	for _, test := range tests {
@@ -108,7 +108,7 @@ func TestCreateBranch(t *testing.T) {
108 108
 		if test.CreateRelease != "" {
109 109
 			createNewRelease(t, session, "/user2/repo1", test.CreateRelease, test.CreateRelease, false, false)
110 110
 		}
111
-		redirectURL := testCreateBranch(t, session, "user2", "repo1", test.OldBranchOrCommit, test.NewBranch, test.ExpectedStatus)
111
+		redirectURL := testCreateBranch(t, session, "user2", "repo1", test.OldRefSubURL, test.NewBranch, test.ExpectedStatus)
112 112
 		if test.ExpectedStatus == http.StatusFound {
113 113
 			req := NewRequest(t, "GET", redirectURL)
114 114
 			resp := session.MakeRequest(t, req, http.StatusOK)
@@ -124,7 +124,7 @@ func TestCreateBranch(t *testing.T) {
124 124
 func TestCreateBranchInvalidCSRF(t *testing.T) {
125 125
 	prepareTestEnv(t)
126 126
 	session := loginUser(t, "user2")
127
-	req := NewRequestWithValues(t, "POST", "user2/repo1/branches/_new/master", map[string]string{
127
+	req := NewRequestWithValues(t, "POST", "user2/repo1/branches/_new/branch/master", map[string]string{
128 128
 		"_csrf":           "fake_csrf",
129 129
 		"new_branch_name": "test",
130 130
 	})

+ 3 - 3
integrations/repo_commits_test.go

@@ -20,7 +20,7 @@ func TestRepoCommits(t *testing.T) {
20 20
 	session := loginUser(t, "user2")
21 21
 
22 22
 	// Request repository commits page
23
-	req := NewRequest(t, "GET", "/user2/repo1/commits/master")
23
+	req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
24 24
 	resp := session.MakeRequest(t, req, http.StatusOK)
25 25
 
26 26
 	doc := NewHTMLParser(t, resp.Body)
@@ -35,7 +35,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
35 35
 	session := loginUser(t, "user2")
36 36
 
37 37
 	// Request repository commits page
38
-	req := NewRequest(t, "GET", "/user2/repo1/commits/master")
38
+	req := NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
39 39
 	resp := session.MakeRequest(t, req, http.StatusOK)
40 40
 
41 41
 	doc := NewHTMLParser(t, resp.Body)
@@ -56,7 +56,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
56 56
 
57 57
 	resp = session.MakeRequest(t, req, http.StatusCreated)
58 58
 
59
-	req = NewRequest(t, "GET", "/user2/repo1/commits/master")
59
+	req = NewRequest(t, "GET", "/user2/repo1/commits/branch/master")
60 60
 	resp = session.MakeRequest(t, req, http.StatusOK)
61 61
 
62 62
 	doc = NewHTMLParser(t, resp.Body)

+ 15 - 6
models/webhook_slack.go

@@ -80,12 +80,22 @@ func SlackLinkFormatter(url string, text string) string {
80 80
 	return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text))
81 81
 }
82 82
 
83
-func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) {
84
-	// created tag/branch
85
-	refName := git.RefEndName(p.Ref)
83
+// SlackLinkToRef slack-formatter link to a repo ref
84
+func SlackLinkToRef(repoURL, ref string) string {
85
+	refName := git.RefEndName(ref)
86
+	switch {
87
+	case strings.HasPrefix(ref, git.BranchPrefix):
88
+		return SlackLinkFormatter(repoURL+"/src/branch/"+refName, refName)
89
+	case strings.HasPrefix(ref, git.TagPrefix):
90
+		return SlackLinkFormatter(repoURL+"/src/tag/"+refName, refName)
91
+	default:
92
+		return SlackLinkFormatter(repoURL+"/src/commit/"+refName, refName)
93
+	}
94
+}
86 95
 
96
+func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) {
87 97
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
88
-	refLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
98
+	refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
89 99
 	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
90 100
 
91 101
 	return &SlackPayload{
@@ -99,7 +109,6 @@ func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayloa
99 109
 func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
100 110
 	// n new commits
101 111
 	var (
102
-		branchName   = git.RefEndName(p.Ref)
103 112
 		commitDesc   string
104 113
 		commitString string
105 114
 	)
@@ -116,7 +125,7 @@ func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, e
116 125
 	}
117 126
 
118 127
 	repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
119
-	branchLink := SlackLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
128
+	branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
120 129
 	text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
121 130
 
122 131
 	var attachmentText string

+ 99 - 21
modules/context/repo.go

@@ -14,6 +14,7 @@ import (
14 14
 	"code.gitea.io/git"
15 15
 	"code.gitea.io/gitea/models"
16 16
 	"code.gitea.io/gitea/modules/cache"
17
+	"code.gitea.io/gitea/modules/log"
17 18
 	"code.gitea.io/gitea/modules/setting"
18 19
 
19 20
 	"github.com/Unknwon/com"
@@ -117,6 +118,20 @@ func (r *Repository) GetCommitsCount() (int64, error) {
117 118
 	})
118 119
 }
119 120
 
121
+// BranchNameSubURL sub-URL for the BranchName field
122
+func (r *Repository) BranchNameSubURL() string {
123
+	switch {
124
+	case r.IsViewBranch:
125
+		return "branch/" + r.BranchName
126
+	case r.IsViewTag:
127
+		return "tag/" + r.BranchName
128
+	case r.IsViewCommit:
129
+		return "commit/" + r.BranchName
130
+	}
131
+	log.Error(4, "Unknown view type for repo: %v", r)
132
+	return ""
133
+}
134
+
120 135
 // GetEditorconfig returns the .editorconfig definition if found in the
121 136
 // HEAD of the default repo branch.
122 137
 func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
@@ -444,8 +459,81 @@ func RepoAssignment() macaron.Handler {
444 459
 	}
445 460
 }
446 461
 
447
-// RepoRef handles repository reference name including those contain `/`.
462
+// RepoRefType type of repo reference
463
+type RepoRefType int
464
+
465
+const (
466
+	// RepoRefLegacy unknown type, make educated guess and redirect.
467
+	// for backward compatibility with previous URL scheme
468
+	RepoRefLegacy RepoRefType = iota
469
+	// RepoRefBranch branch
470
+	RepoRefBranch
471
+	// RepoRefTag tag
472
+	RepoRefTag
473
+	// RepoRefCommit commit
474
+	RepoRefCommit
475
+)
476
+
477
+// RepoRef handles repository reference names when the ref name is not
478
+// explicitly given
448 479
 func RepoRef() macaron.Handler {
480
+	// since no ref name is explicitly specified, ok to just use branch
481
+	return RepoRefByType(RepoRefBranch)
482
+}
483
+
484
+func getRefNameFromPath(ctx *Context, path string, isExist func(string) bool) string {
485
+	refName := ""
486
+	parts := strings.Split(path, "/")
487
+	for i, part := range parts {
488
+		refName = strings.TrimPrefix(refName+"/"+part, "/")
489
+		if isExist(refName) {
490
+			ctx.Repo.TreePath = strings.Join(parts[i+1:], "/")
491
+			return refName
492
+		}
493
+	}
494
+	return ""
495
+}
496
+
497
+func getRefName(ctx *Context, pathType RepoRefType) string {
498
+	path := ctx.Params("*")
499
+	switch pathType {
500
+	case RepoRefLegacy:
501
+		if refName := getRefName(ctx, RepoRefBranch); len(refName) > 0 {
502
+			return refName
503
+		}
504
+		if refName := getRefName(ctx, RepoRefTag); len(refName) > 0 {
505
+			return refName
506
+		}
507
+		return getRefName(ctx, RepoRefCommit)
508
+	case RepoRefBranch:
509
+		return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsBranchExist)
510
+	case RepoRefTag:
511
+		return getRefNameFromPath(ctx, path, ctx.Repo.GitRepo.IsTagExist)
512
+	case RepoRefCommit:
513
+		parts := strings.Split(path, "/")
514
+		if len(parts) > 0 && len(parts[0]) == 40 {
515
+			ctx.Repo.TreePath = strings.Join(parts[1:], "/")
516
+			return parts[0]
517
+		}
518
+	default:
519
+		log.Error(4, "Unrecognized path type: %v", path)
520
+	}
521
+	return ""
522
+}
523
+
524
+// URL to redirect to for deprecated URL scheme
525
+func repoRefRedirect(ctx *Context) string {
526
+	urlPath := ctx.Req.URL.String()
527
+	idx := strings.LastIndex(urlPath, ctx.Params("*"))
528
+	if idx < 0 {
529
+		idx = len(urlPath)
530
+	}
531
+	return path.Join(urlPath[:idx], ctx.Repo.BranchNameSubURL())
532
+}
533
+
534
+// RepoRefByType handles repository reference name for a specific type
535
+// of repository reference
536
+func RepoRefByType(refType RepoRefType) macaron.Handler {
449 537
 	return func(ctx *Context) {
450 538
 		// Empty repository does not have reference information.
451 539
 		if ctx.Repo.Repository.IsBare {
@@ -470,6 +558,7 @@ func RepoRef() macaron.Handler {
470 558
 		// Get default branch.
471 559
 		if len(ctx.Params("*")) == 0 {
472 560
 			refName = ctx.Repo.Repository.DefaultBranch
561
+			ctx.Repo.BranchName = refName
473 562
 			if !ctx.Repo.GitRepo.IsBranchExist(refName) {
474 563
 				brs, err := ctx.Repo.GitRepo.GetBranches()
475 564
 				if err != nil {
@@ -492,25 +581,8 @@ func RepoRef() macaron.Handler {
492 581
 			ctx.Repo.IsViewBranch = true
493 582
 
494 583
 		} else {
495
-			hasMatched := false
496
-			parts := strings.Split(ctx.Params("*"), "/")
497
-			for i, part := range parts {
498
-				refName = strings.TrimPrefix(refName+"/"+part, "/")
499
-
500
-				if ctx.Repo.GitRepo.IsBranchExist(refName) ||
501
-					ctx.Repo.GitRepo.IsTagExist(refName) {
502
-					if i < len(parts)-1 {
503
-						ctx.Repo.TreePath = strings.Join(parts[i+1:], "/")
504
-					}
505
-					hasMatched = true
506
-					break
507
-				}
508
-			}
509
-			if !hasMatched && len(parts[0]) == 40 {
510
-				refName = parts[0]
511
-				ctx.Repo.TreePath = strings.Join(parts[1:], "/")
512
-			}
513
-
584
+			refName = getRefName(ctx, refType)
585
+			ctx.Repo.BranchName = refName
514 586
 			if ctx.Repo.GitRepo.IsBranchExist(refName) {
515 587
 				ctx.Repo.IsViewBranch = true
516 588
 
@@ -542,10 +614,16 @@ func RepoRef() macaron.Handler {
542 614
 				ctx.Handle(404, "RepoRef invalid repo", fmt.Errorf("branch or tag not exist: %s", refName))
543 615
 				return
544 616
 			}
617
+
618
+			if refType == RepoRefLegacy {
619
+				// redirect from old URL scheme to new URL scheme
620
+				ctx.Redirect(repoRefRedirect(ctx))
621
+				return
622
+			}
545 623
 		}
546 624
 
547
-		ctx.Repo.BranchName = refName
548 625
 		ctx.Data["BranchName"] = ctx.Repo.BranchName
626
+		ctx.Data["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL()
549 627
 		ctx.Data["CommitID"] = ctx.Repo.CommitID
550 628
 		ctx.Data["TreePath"] = ctx.Repo.TreePath
551 629
 		ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch

+ 1 - 1
routers/api/v1/api.go

@@ -391,7 +391,7 @@ func RegisterRoutes(m *macaron.Macaron) {
391 391
 					Post(reqToken(), bind(api.CreateForkOption{}), repo.CreateFork)
392 392
 				m.Group("/branches", func() {
393 393
 					m.Get("", repo.ListBranches)
394
-					m.Get("/*", context.RepoRef(), repo.GetBranch)
394
+					m.Get("/*", context.RepoRefByType(context.RepoRefBranch), repo.GetBranch)
395 395
 				})
396 396
 				m.Group("/keys", func() {
397 397
 					m.Combo("").Get(repo.ListDeployKeys).

+ 5 - 5
routers/repo/branch.go

@@ -202,7 +202,7 @@ func CreateBranch(ctx *context.Context, form auth.NewBranchForm) {
202 202
 
203 203
 	if ctx.HasError() {
204 204
 		ctx.Flash.Error(ctx.GetErrMsg())
205
-		ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName)
205
+		ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
206 206
 		return
207 207
 	}
208 208
 
@@ -216,19 +216,19 @@ func CreateBranch(ctx *context.Context, form auth.NewBranchForm) {
216 216
 		if models.IsErrTagAlreadyExists(err) {
217 217
 			e := err.(models.ErrTagAlreadyExists)
218 218
 			ctx.Flash.Error(ctx.Tr("repo.branch.tag_collision", e.TagName))
219
-			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName)
219
+			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
220 220
 			return
221 221
 		}
222 222
 		if models.IsErrBranchAlreadyExists(err) {
223 223
 			e := err.(models.ErrBranchAlreadyExists)
224 224
 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", e.BranchName))
225
-			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName)
225
+			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
226 226
 			return
227 227
 		}
228 228
 		if models.IsErrBranchNameConflict(err) {
229 229
 			e := err.(models.ErrBranchNameConflict)
230 230
 			ctx.Flash.Error(ctx.Tr("repo.branch.branch_name_conflict", form.NewBranchName, e.BranchName))
231
-			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName)
231
+			ctx.Redirect(ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL())
232 232
 			return
233 233
 		}
234 234
 
@@ -237,5 +237,5 @@ func CreateBranch(ctx *context.Context, form auth.NewBranchForm) {
237 237
 	}
238 238
 
239 239
 	ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName))
240
-	ctx.Redirect(ctx.Repo.RepoLink + "/src/" + form.NewBranchName)
240
+	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + form.NewBranchName)
241 241
 }

+ 1 - 1
routers/repo/commit.go

@@ -120,7 +120,7 @@ func SearchCommits(ctx *context.Context) {
120 120
 
121 121
 	keyword := strings.Trim(ctx.Query("q"), " ")
122 122
 	if len(keyword) == 0 {
123
-		ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchName)
123
+		ctx.Redirect(ctx.Repo.RepoLink + "/commits/" + ctx.Repo.BranchNameSubURL())
124 124
 		return
125 125
 	}
126 126
 	all := ctx.QueryBool("all")

+ 9 - 9
routers/repo/editor.go

@@ -113,7 +113,7 @@ func editFile(ctx *context.Context, isNewFile bool) {
113 113
 
114 114
 	ctx.Data["TreeNames"] = treeNames
115 115
 	ctx.Data["TreePaths"] = treePaths
116
-	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName
116
+	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
117 117
 	ctx.Data["commit_summary"] = ""
118 118
 	ctx.Data["commit_message"] = ""
119 119
 	if canCommit {
@@ -164,7 +164,7 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
164 164
 	ctx.Data["TreePath"] = form.TreePath
165 165
 	ctx.Data["TreeNames"] = treeNames
166 166
 	ctx.Data["TreePaths"] = treePaths
167
-	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + branchName
167
+	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
168 168
 	ctx.Data["FileContent"] = form.Content
169 169
 	ctx.Data["commit_summary"] = form.CommitSummary
170 170
 	ctx.Data["commit_message"] = form.CommitMessage
@@ -304,7 +304,7 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
304 304
 		return
305 305
 	}
306 306
 
307
-	ctx.Redirect(ctx.Repo.RepoLink + "/src/" + branchName + "/" + strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(form.TreePath))
307
+	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + branchName + "/" + strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(form.TreePath))
308 308
 }
309 309
 
310 310
 // EditFilePost response for editing file
@@ -348,7 +348,7 @@ func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) {
348 348
 // DeleteFile render delete file page
349 349
 func DeleteFile(ctx *context.Context) {
350 350
 	ctx.Data["PageIsDelete"] = true
351
-	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName
351
+	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
352 352
 	ctx.Data["TreePath"] = ctx.Repo.TreePath
353 353
 	canCommit := renderCommitRights(ctx)
354 354
 
@@ -367,7 +367,7 @@ func DeleteFile(ctx *context.Context) {
367 367
 // DeleteFilePost response for deleting file
368 368
 func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
369 369
 	ctx.Data["PageIsDelete"] = true
370
-	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName
370
+	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
371 371
 	ctx.Data["TreePath"] = ctx.Repo.TreePath
372 372
 	canCommit := renderCommitRights(ctx)
373 373
 
@@ -422,7 +422,7 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
422 422
 	}
423 423
 
424 424
 	ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
425
-	ctx.Redirect(ctx.Repo.RepoLink + "/src/" + branchName)
425
+	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + branchName)
426 426
 }
427 427
 
428 428
 func renderUploadSettings(ctx *context.Context) {
@@ -446,7 +446,7 @@ func UploadFile(ctx *context.Context) {
446 446
 
447 447
 	ctx.Data["TreeNames"] = treeNames
448 448
 	ctx.Data["TreePaths"] = treePaths
449
-	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName
449
+	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
450 450
 	ctx.Data["commit_summary"] = ""
451 451
 	ctx.Data["commit_message"] = ""
452 452
 	if canCommit {
@@ -482,7 +482,7 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) {
482 482
 	ctx.Data["TreePath"] = form.TreePath
483 483
 	ctx.Data["TreeNames"] = treeNames
484 484
 	ctx.Data["TreePaths"] = treePaths
485
-	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + branchName
485
+	ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
486 486
 	ctx.Data["commit_summary"] = form.CommitSummary
487 487
 	ctx.Data["commit_message"] = form.CommitMessage
488 488
 	ctx.Data["commit_choice"] = form.CommitChoice
@@ -551,7 +551,7 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) {
551 551
 		return
552 552
 	}
553 553
 
554
-	ctx.Redirect(ctx.Repo.RepoLink + "/src/" + branchName + "/" + form.TreePath)
554
+	ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + branchName + "/" + form.TreePath)
555 555
 }
556 556
 
557 557
 // UploadFileToServer upload file to server file dir not git

+ 15 - 0
routers/repo/repo.go

@@ -34,6 +34,21 @@ func MustBeNotBare(ctx *context.Context) {
34 34
 	}
35 35
 }
36 36
 
37
+// MustBeEditable check that repo can be edited
38
+func MustBeEditable(ctx *context.Context) {
39
+	if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
40
+		ctx.Handle(404, "", nil)
41
+		return
42
+	}
43
+}
44
+
45
+// MustBeAbleToUpload check that repo can be uploaded to
46
+func MustBeAbleToUpload(ctx *context.Context) {
47
+	if !setting.Repository.Upload.Enabled {
48
+		ctx.Handle(404, "", nil)
49
+	}
50
+}
51
+
37 52
 func checkContextUser(ctx *context.Context, uid int64) *models.User {
38 53
 	orgs, err := models.GetOwnedOrgsByUserIDDesc(ctx.User.ID, "updated_unix")
39 54
 	if err != nil {

+ 2 - 2
routers/repo/view.go

@@ -297,9 +297,9 @@ func renderCode(ctx *context.Context) {
297 297
 	ctx.Data["Title"] = title
298 298
 	ctx.Data["RequireHighlightJS"] = true
299 299
 
300
-	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchName
300
+	branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
301 301
 	treeLink := branchLink
302
-	rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchName
302
+	rawLink := ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL()
303 303
 
304 304
 	if len(ctx.Repo.TreePath) > 0 {
305 305
 		treeLink += "/" + ctx.Repo.TreePath

+ 42 - 25
routers/routes/routes.go

@@ -522,34 +522,30 @@ func RegisterRoutes(m *macaron.Macaron) {
522 522
 			Post(bindIgnErr(auth.CreateIssueForm{}), repo.CompareAndPullRequestPost)
523 523
 
524 524
 		m.Group("", func() {
525
-			m.Combo("/_edit/*").Get(repo.EditFile).
526
-				Post(bindIgnErr(auth.EditRepoFileForm{}), repo.EditFilePost)
527
-			m.Combo("/_new/*").Get(repo.NewFile).
528
-				Post(bindIgnErr(auth.EditRepoFileForm{}), repo.NewFilePost)
529
-			m.Post("/_preview/*", bindIgnErr(auth.EditPreviewDiffForm{}), repo.DiffPreviewPost)
530
-			m.Combo("/_delete/*").Get(repo.DeleteFile).
531
-				Post(bindIgnErr(auth.DeleteRepoFileForm{}), repo.DeleteFilePost)
532
-
533 525
 			m.Group("", func() {
534
-				m.Combo("/_upload/*").Get(repo.UploadFile).
526
+				m.Combo("/_edit/*").Get(repo.EditFile).
527
+					Post(bindIgnErr(auth.EditRepoFileForm{}), repo.EditFilePost)
528
+				m.Combo("/_new/*").Get(repo.NewFile).
529
+					Post(bindIgnErr(auth.EditRepoFileForm{}), repo.NewFilePost)
530
+				m.Post("/_preview/*", bindIgnErr(auth.EditPreviewDiffForm{}), repo.DiffPreviewPost)
531
+				m.Combo("/_delete/*").Get(repo.DeleteFile).
532
+					Post(bindIgnErr(auth.DeleteRepoFileForm{}), repo.DeleteFilePost)
533
+				m.Combo("/_upload/*", repo.MustBeAbleToUpload).
534
+					Get(repo.UploadFile).
535 535
 					Post(bindIgnErr(auth.UploadRepoFileForm{}), repo.UploadFilePost)
536
+			}, context.RepoRefByType(context.RepoRefBranch), repo.MustBeEditable)
537
+			m.Group("", func() {
536 538
 				m.Post("/upload-file", repo.UploadFileToServer)
537 539
 				m.Post("/upload-remove", bindIgnErr(auth.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer)
538
-			}, func(ctx *context.Context) {
539
-				if !setting.Repository.Upload.Enabled {
540
-					ctx.Handle(404, "", nil)
541
-					return
542
-				}
543
-			})
544
-		}, repo.MustBeNotBare, reqRepoWriter, context.RepoRef(), func(ctx *context.Context) {
545
-			if !ctx.Repo.Repository.CanEnableEditor() || ctx.Repo.IsViewCommit {
546
-				ctx.Handle(404, "", nil)
547
-				return
548
-			}
549
-		})
540
+			}, context.RepoRef(), repo.MustBeEditable, repo.MustBeAbleToUpload)
541
+		}, repo.MustBeNotBare, reqRepoWriter)
550 542
 
551 543
 		m.Group("/branches", func() {
552
-			m.Post("/_new/*", context.RepoRef(), bindIgnErr(auth.NewBranchForm{}), repo.CreateBranch)
544
+			m.Group("/_new/", func() {
545
+				m.Post("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.CreateBranch)
546
+				m.Post("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.CreateBranch)
547
+				m.Post("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.CreateBranch)
548
+			}, bindIgnErr(auth.NewBranchForm{}))
553 549
 			m.Post("/delete", repo.DeleteBranchPost)
554 550
 			m.Post("/restore", repo.RestoreBranchPost)
555 551
 		}, reqRepoWriter, repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode))
@@ -629,15 +625,36 @@ func RegisterRoutes(m *macaron.Macaron) {
629 625
 			m.Post("/cleanup", context.RepoRef(), repo.CleanUpPullRequest)
630 626
 		}, repo.MustAllowPulls)
631 627
 
628
+		m.Group("/raw", func() {
629
+			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.SingleDownload)
630
+			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.SingleDownload)
631
+			m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.SingleDownload)
632
+			// "/*" route is deprecated, and kept for backward compatibility
633
+			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.SingleDownload)
634
+		}, repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode))
635
+
636
+		m.Group("/commits", func() {
637
+			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.RefCommits)
638
+			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.RefCommits)
639
+			m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.RefCommits)
640
+			// "/*" route is deprecated, and kept for backward compatibility
641
+			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.RefCommits)
642
+		}, repo.MustBeNotBare, context.CheckUnit(models.UnitTypeCode))
643
+
632 644
 		m.Group("", func() {
633
-			m.Get("/raw/*", repo.SingleDownload)
634
-			m.Get("/commits/*", repo.RefCommits)
635 645
 			m.Get("/graph", repo.Graph)
636 646
 			m.Get("/commit/:sha([a-f0-9]{7,40})$", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.Diff)
637 647
 		}, repo.MustBeNotBare, context.RepoRef(), context.CheckUnit(models.UnitTypeCode))
638 648
 
649
+		m.Group("/src", func() {
650
+			m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
651
+			m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.Home)
652
+			m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.Home)
653
+			// "/*" route is deprecated, and kept for backward compatibility
654
+			m.Get("/*", context.RepoRefByType(context.RepoRefLegacy), repo.Home)
655
+		}, repo.SetEditorconfigIfExists)
656
+
639 657
 		m.Group("", func() {
640
-			m.Get("/src/*", repo.SetEditorconfigIfExists, repo.Home)
641 658
 			m.Get("/forks", repo.Forks)
642 659
 		}, context.RepoRef(), context.CheckUnit(models.UnitTypeCode))
643 660
 		m.Get("/commit/:sha([a-f0-9]{7,40})\\.:ext(patch|diff)",

+ 2 - 2
templates/repo/branch_dropdown.tmpl

@@ -10,10 +10,10 @@
10 10
 		</div>
11 11
 		<div class="data" style="display: none" data-mode="{{if .IsViewTag}}tags{{else}}branches{{end}}">
12 12
 			{{range .Branches}}
13
-				<div class="item branch {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div>
13
+				<div class="item branch {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/branch/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div>
14 14
 			{{end}}
15 15
 			{{range .Tags}}
16
-				<div class="item tag {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div>
16
+				<div class="item tag {{if eq $.BranchName .}}selected{{end}}" data-url="{{$.RepoLink}}/{{if $.PageIsCommits}}commits{{else}}src{{end}}/tag/{{EscapePound .}}{{if $.TreePath}}/{{EscapePound $.TreePath}}{{end}}">{{.}}</div>
17 17
 			{{end}}
18 18
 		</div>
19 19
 		<div class="menu transition" :class="{visible: menuVisible}" v-if="menuVisible" v-cloak>

+ 4 - 4
templates/repo/commits_table.tmpl

@@ -2,7 +2,7 @@
2 2
 	{{.CommitCount}} {{.i18n.Tr "repo.commits.commits"}} {{if .Branch}}({{.Branch}}){{end}}
3 3
 	{{if .PageIsCommits}}
4 4
 		<div class="ui right">
5
-			<form action="{{.RepoLink}}/commits/{{.BranchName}}/search">
5
+			<form action="{{.RepoLink}}/commits/{{.BranchNameSubURL}}/search">
6 6
 				<div class="ui tiny search input">
7 7
 					<input name="q" placeholder="{{.i18n.Tr "repo.commits.search"}}" value="{{.Keyword}}" autofocus>
8 8
 				</div>
@@ -75,17 +75,17 @@
75 75
 	{{if gt .TotalPages 1}}
76 76
 		<div class="center page buttons">
77 77
 			<div class="ui borderless pagination menu">
78
-				<a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.RepoLink}}/commits/{{$.BranchName}}{{if $.FileName}}/{{$.FileName}}{{end}}?page={{.Previous}}"{{end}}>
78
+				<a class="{{if not .HasPrevious}}disabled{{end}} item" {{if .HasPrevious}}href="{{$.RepoLink}}/commits/{{$.BranchNameSubURL}}{{if $.FileName}}/{{$.FileName}}{{end}}?page={{.Previous}}"{{end}}>
79 79
 					<i class="left arrow icon"></i> {{$.i18n.Tr "repo.issues.previous"}}
80 80
 				</a>
81 81
 				{{range .Pages}}
82 82
 					{{if eq .Num -1}}
83 83
 						<a class="disabled item">...</a>
84 84
 					{{else}}
85
-						<a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.RepoLink}}/commits/{{$.BranchName}}{{if $.FileName}}/{{$.FileName}}{{end}}?page={{.Num}}"{{end}}>{{.Num}}</a>
85
+						<a class="{{if .IsCurrent}}active{{end}} item" {{if not .IsCurrent}}href="{{$.RepoLink}}/commits/{{$.BranchNameSubURL}}{{if $.FileName}}/{{$.FileName}}{{end}}?page={{.Num}}"{{end}}>{{.Num}}</a>
86 86
 					{{end}}
87 87
 				{{end}}
88
-				<a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.RepoLink}}/commits/{{$.BranchName}}{{if $.FileName}}/{{$.FileName}}{{end}}?page={{.Next}}"{{end}}>
88
+				<a class="{{if not .HasNext}}disabled{{end}} item" {{if .HasNext}}href="{{$.RepoLink}}/commits/{{$.BranchNameSubURL}}{{if $.FileName}}/{{$.FileName}}{{end}}?page={{.Next}}"{{end}}>
89 89
 					{{$.i18n.Tr "repo.issues.next"}} <i class="icon right arrow"></i>
90 90
 				</a>
91 91
 			</div>

+ 1 - 1
templates/repo/editor/edit.tmpl

@@ -30,7 +30,7 @@
30 30
 				<div class="ui top attached tabular menu" data-write="write" data-preview="preview" data-diff="diff">
31 31
 					<a class="active item" data-tab="write"><i class="octicon octicon-code"></i> {{if .IsNewFile}}{{.i18n.Tr "repo.editor.new_file"}}{{else}}{{.i18n.Tr "repo.editor.edit_file"}}{{end}}</a>
32 32
 					{{if not .IsNewFile}}
33
-					<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}/src/{{.BranchName}}" data-preview-file-modes="{{.PreviewableFileModes}}"><i class="octicon octicon-eye"></i> {{.i18n.Tr "repo.release.preview"}}</a>
33
+					<a class="item" data-tab="preview" data-url="{{AppSubUrl}}/api/v1/markdown" data-context="{{.RepoLink}}/src/{{.BranchNameSubURL}}" data-preview-file-modes="{{.PreviewableFileModes}}"><i class="octicon octicon-eye"></i> {{.i18n.Tr "repo.release.preview"}}</a>
34 34
 					<a class="item" data-tab="diff" data-url="{{.RepoLink}}/_preview/{{.BranchName}}/{{.TreePath}}" data-context="{{.BranchLink}}"><i class="octicon octicon-diff"></i> {{.i18n.Tr "repo.editor.preview_changes"}}</a>
35 35
 					{{end}}
36 36
 				</div>

+ 1 - 1
templates/repo/home.tmpl

@@ -35,7 +35,7 @@
35 35
 			{{template "repo/branch_dropdown" .}}
36 36
 			{{ $n := len .TreeNames}}
37 37
 			{{ $l := Subtract $n 1}}
38
-			<div class="fitted item"><span class="ui breadcrumb repo-path"><a class="section" href="{{.RepoLink}}/src/{{EscapePound .BranchName}}">{{EllipsisString .Repository.Name 30}}</a>{{range $i, $v := .TreeNames}}<span class="divider">/</span>{{if eq $i $l}}<span class="active section">{{EllipsisString $v 30}}</span>{{else}}{{ $p := index $.Paths $i}}<span class="section"><a href="{{EscapePound $.BranchLink}}/{{EscapePound $p}}">{{EllipsisString $v 30}}</a></span>{{end}}{{end}}</span></div>
38
+			<div class="fitted item"><span class="ui breadcrumb repo-path"><a class="section" href="{{.RepoLink}}/src/{{EscapePound .BranchNameSubURL}}">{{EllipsisString .Repository.Name 30}}</a>{{range $i, $v := .TreeNames}}<span class="divider">/</span>{{if eq $i $l}}<span class="active section">{{EllipsisString $v 30}}</span>{{else}}{{ $p := index $.Paths $i}}<span class="section"><a href="{{EscapePound $.BranchLink}}/{{EscapePound $p}}">{{EllipsisString $v 30}}</a></span>{{end}}{{end}}</span></div>
39 39
 			<div class="right fitted item">
40 40
 				{{if .Repository.CanEnableEditor}}
41 41
 					<div id="file-buttons" class="ui tiny blue buttons">

+ 1 - 1
templates/repo/issue/list.tmpl

@@ -172,7 +172,7 @@
172 172
 					<a class="title has-emoji" href="{{$.Link}}/{{.Index}}">{{.Title}}</a>
173 173
 
174 174
 					{{if .Ref}}
175
-						<a class="ui label" href="{{$.RepoLink}}/src/{{.Ref}}">{{.Ref}}</a>
175
+						<a class="ui label" href="{{$.RepoLink}}/src/commit/{{.Ref}}">{{.Ref}}</a>
176 176
 					{{end}}
177 177
 					{{range .Labels}}
178 178
 						<a class="ui label" href="{{$.Link}}?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}&milestone={{$.MilestoneID}}&assignee={{$.AssigneeID}}" style="color: {{.ForegroundColor}}; background-color: {{.Color}}">{{.Name | Sanitize}}</a>

+ 5 - 5
templates/repo/release/list.tmpl

@@ -28,26 +28,26 @@
28 28
 								<span class="ui green label">{{$.i18n.Tr "repo.release.stable"}}</span>
29 29
 							{{end}}
30 30
 							<span class="tag text blue">
31
-								<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
31
+								<a href="{{$.RepoLink}}/src/tag/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
32 32
 							</span>
33 33
 							<span class="commit">
34
-								<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
34
+								<a href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
35 35
 							</span>
36 36
 						{{end}}
37 37
 					</div>
38 38
 					<div class="ui twelve wide column detail">
39 39
 						{{if .IsTag}}
40 40
 							<h4>
41
-								<a href="{{$.RepoLink}}/src/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
41
+								<a href="{{$.RepoLink}}/src/tag/{{.TagName}}" rel="nofollow"><i class="tag icon"></i> {{.TagName}}</a>
42 42
 							</h4>
43 43
 							<div class="download">
44
-								<a href="{{$.RepoLink}}/src/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
44
+								<a href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow"><i class="code icon"></i> {{ShortSha .Sha1}}</a>
45 45
 								<a href="{{$.RepoLink}}/archive/{{.TagName}}.zip" rel="nofollow"><i class="octicon octicon-file-zip"></i> ZIP</a>
46 46
 								<a href="{{$.RepoLink}}/archive/{{.TagName}}.tar.gz"><i class="octicon octicon-file-zip"></i> TAR.GZ</a>
47 47
 							</div>
48 48
 						{{else}}
49 49
 							<h3>
50
-								<a href="{{$.RepoLink}}/src/{{.TagName}}">{{.Title}}</a>
50
+								<a href="{{$.RepoLink}}/src/tag/{{.TagName}}">{{.Title}}</a>
51 51
 								{{if $.IsRepositoryWriter}}<small>(<a href="{{$.RepoLink}}/releases/edit/{{.TagName}}" rel="nofollow">{{$.i18n.Tr "repo.release.edit"}}</a>)</small>{{end}}
52 52
 							</h3>
53 53
 							<p class="text grey">

+ 2 - 2
templates/repo/view_file.tmpl

@@ -15,9 +15,9 @@
15 15
 			<div class="ui right file-actions">
16 16
 				<div class="ui buttons">
17 17
 					{{if not .IsViewCommit}}
18
-						<a class="ui button" href="{{.RepoLink}}/src/{{.CommitID}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_permalink"}}</a>
18
+						<a class="ui button" href="{{.RepoLink}}/src/commit/{{.CommitID}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_permalink"}}</a>
19 19
 					{{end}}
20
-					<a class="ui button" href="{{.RepoLink}}/commits/{{EscapePound .BranchName}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_history"}}</a>
20
+					<a class="ui button" href="{{.RepoLink}}/commits/{{EscapePound .BranchNameSubURL}}/{{EscapePound .TreePath}}">{{.i18n.Tr "repo.file_history"}}</a>
21 21
 					<a class="ui button" href="{{EscapePound $.RawFileLink}}">{{.i18n.Tr "repo.file_raw"}}</a>
22 22
 				</div>
23 23
 				{{if .Repository.CanEnableEditor}}