forked from Gitlink/gitea-1120-rc1
files、commits、issues、compare for pull reqeust
This commit is contained in:
commit
da051e6417
|
@ -506,6 +506,10 @@ func mustNotBeArchived(ctx *context.APIContext) {
|
||||||
func RegisterRoutes(m *macaron.Macaron) {
|
func RegisterRoutes(m *macaron.Macaron) {
|
||||||
bind := binding.Bind
|
bind := binding.Bind
|
||||||
|
|
||||||
|
// add by qiubing
|
||||||
|
reqRepoCodeReader := context.RequireRepoReader(models.UnitTypeCode)
|
||||||
|
// end by qiubing
|
||||||
|
|
||||||
if setting.API.EnableSwagger {
|
if setting.API.EnableSwagger {
|
||||||
m.Get("/swagger", misc.Swagger) // Render V1 by default
|
m.Get("/swagger", misc.Swagger) // Render V1 by default
|
||||||
}
|
}
|
||||||
|
@ -676,6 +680,17 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.GetCommitsCount)
|
m.Get("/tag/*", context.RepoRefByType(context.RepoRefTag), repo.GetCommitsCount)
|
||||||
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.GetCommitsCount)
|
m.Get("/commit/*", context.RepoRefByType(context.RepoRefCommit), repo.GetCommitsCount)
|
||||||
})
|
})
|
||||||
|
m.Group("/pulls/:index", func() {
|
||||||
|
m.Get("/commits", context.RepoRef(), repo.GetPullCommits)
|
||||||
|
m.Get("/files", context.RepoRef(), repo.GetPullFiles)
|
||||||
|
m.Get("/issues", context.RepoRef(), repo.GetPullIssues)
|
||||||
|
})
|
||||||
|
// m.Group("/compare", func() {
|
||||||
|
m.Get("/compare/*", context.RepoAssignment(), repo.MustBeNotEmpty, reqRepoCodeReader,
|
||||||
|
repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.CompareDiff)
|
||||||
|
// })
|
||||||
|
// m.Combo("/compare/*", context.RepoRef(), repo.MustBeNotEmpty, reqRepoCodeReader, repo.SetEditorconfigIfExists).
|
||||||
|
// Get(repo.SetDiffViewStyle, repo.CompareDiff)
|
||||||
//end by qiubing
|
//end by qiubing
|
||||||
|
|
||||||
m.Group("/collaborators", func() {
|
m.Group("/collaborators", func() {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -17,9 +18,11 @@ import (
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/notification"
|
"code.gitea.io/gitea/modules/notification"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
issue_service "code.gitea.io/gitea/services/issue"
|
issue_service "code.gitea.io/gitea/services/issue"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
)
|
)
|
||||||
|
@ -169,6 +172,411 @@ func GetPullRequest(ctx *context.APIContext) {
|
||||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(pr))
|
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(pr))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add by qiubing
|
||||||
|
func GetPullIssues(ctx *context.APIContext) {
|
||||||
|
issue := checkPullInfo(ctx.Context)
|
||||||
|
if issue == nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies, err := issue.BlockingDependencies()
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("BlockingDependencies", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, dependencies)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPullFiles(ctx *context.APIContext) {
|
||||||
|
issue := checkPullInfo(ctx.Context)
|
||||||
|
if issue == nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pull := issue.PullRequest
|
||||||
|
|
||||||
|
whitespaceFlags := map[string]string{
|
||||||
|
"ignore-all": "-w",
|
||||||
|
"ignore-change": "-b",
|
||||||
|
"ignore-eol": "--ignore-space-at-eol",
|
||||||
|
"": ""}
|
||||||
|
|
||||||
|
var (
|
||||||
|
diffRepoPath string
|
||||||
|
startCommitID string
|
||||||
|
endCommitID string
|
||||||
|
gitRepo *git.Repository
|
||||||
|
)
|
||||||
|
|
||||||
|
var prInfo *git.CompareInfo
|
||||||
|
if pull.HasMerged {
|
||||||
|
prInfo = PrepareMergedViewPullInfo(ctx.Context, issue)
|
||||||
|
} else {
|
||||||
|
prInfo = PrepareViewPullInfo(ctx.Context, issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
} else if prInfo == nil {
|
||||||
|
ctx.NotFound("ViewPullFiles", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diffRepoPath = ctx.Repo.GitRepo.Path
|
||||||
|
gitRepo = ctx.Repo.GitRepo
|
||||||
|
|
||||||
|
headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetRefCommitID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
startCommitID = prInfo.MergeBase
|
||||||
|
endCommitID = headCommitID
|
||||||
|
|
||||||
|
ctx.Data["WhitespaceBehavior"] = ""
|
||||||
|
diff, err := gitdiff.GetDiffRangeWithWhitespaceBehavior(diffRepoPath,
|
||||||
|
startCommitID, endCommitID, setting.Git.MaxGitDiffLines,
|
||||||
|
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles,
|
||||||
|
whitespaceFlags[ctx.Data["WhitespaceBehavior"].(string)])
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = diff.LoadComments(issue, ctx.User); err != nil {
|
||||||
|
ctx.ServerError("LoadComments", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPullCommits(ctx *context.APIContext) {
|
||||||
|
issue := checkPullInfo(ctx.Context)
|
||||||
|
if issue == nil {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pull := issue.PullRequest
|
||||||
|
|
||||||
|
var commits *list.List
|
||||||
|
var prInfo *git.CompareInfo
|
||||||
|
if pull.HasMerged {
|
||||||
|
prInfo = PrepareMergedViewPullInfo(ctx.Context, issue)
|
||||||
|
} else {
|
||||||
|
prInfo = PrepareViewPullInfo(ctx.Context, issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
} else if prInfo == nil {
|
||||||
|
ctx.NotFound("ViewPullCommits", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||||
|
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
|
||||||
|
commits = prInfo.Commits
|
||||||
|
commits = models.ValidateCommitsWithEmails(commits)
|
||||||
|
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
|
||||||
|
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
|
||||||
|
ctx.Data["Commits"] = commits
|
||||||
|
ctx.Data["CommitCount"] = commits.Len()
|
||||||
|
|
||||||
|
result := make([]models.SignCommitWithStatuses, 0)
|
||||||
|
for commit := commits.Front(); commit != nil; commit = commit.Next() {
|
||||||
|
temp := commit.Value.(models.SignCommitWithStatuses)
|
||||||
|
result = append(result, temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
|
||||||
|
repo := ctx.Repo.Repository
|
||||||
|
pull := issue.PullRequest
|
||||||
|
|
||||||
|
if err := pull.LoadHeadRepo(); err != nil {
|
||||||
|
ctx.ServerError("LoadHeadRepo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pull.LoadBaseRepo(); err != nil {
|
||||||
|
ctx.ServerError("LoadBaseRepo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
setMergeTarget(ctx, pull)
|
||||||
|
|
||||||
|
if err := pull.LoadProtectedBranch(); err != nil {
|
||||||
|
ctx.ServerError("LoadProtectedBranch", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["EnableStatusCheck"] = pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck
|
||||||
|
|
||||||
|
baseGitRepo, err := git.OpenRepository(pull.BaseRepo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("OpenRepository", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer baseGitRepo.Close()
|
||||||
|
|
||||||
|
if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
|
||||||
|
ctx.Data["IsPullRequestBroken"] = true
|
||||||
|
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||||
|
ctx.Data["HeadTarget"] = pull.HeadBranch
|
||||||
|
|
||||||
|
sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
commitStatuses, err := models.GetLatestCommitStatus(repo, sha, 0)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLatestCommitStatus", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(commitStatuses) > 0 {
|
||||||
|
ctx.Data["LatestCommitStatuses"] = commitStatuses
|
||||||
|
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
|
||||||
|
pull.MergeBase, pull.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||||
|
ctx.Data["IsPullRequestBroken"] = true
|
||||||
|
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||||
|
ctx.Data["NumCommits"] = 0
|
||||||
|
ctx.Data["NumFiles"] = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ServerError("GetCompareInfo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["NumCommits"] = compareInfo.Commits.Len()
|
||||||
|
ctx.Data["NumFiles"] = compareInfo.NumFiles
|
||||||
|
return compareInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
var headBranchExist bool
|
||||||
|
var headBranchSha string
|
||||||
|
// HeadRepo may be missing
|
||||||
|
if pull.HeadRepo != nil {
|
||||||
|
headGitRepo, err := git.OpenRepository(pull.HeadRepo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("OpenRepository", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer headGitRepo.Close()
|
||||||
|
|
||||||
|
headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
|
||||||
|
|
||||||
|
if headBranchExist {
|
||||||
|
headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBranchCommitID", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if headBranchExist {
|
||||||
|
ctx.Data["UpdateAllowed"], err = pull_service.IsUserAllowedToUpdate(pull, ctx.User)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("IsUserAllowedToUpdate", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["GetCommitMessages"] = pull_service.GetCommitMessages(pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
if git.IsErrNotExist(err) {
|
||||||
|
ctx.Data["IsPullRequestBroken"] = true
|
||||||
|
if pull.IsSameRepo() {
|
||||||
|
ctx.Data["HeadTarget"] = pull.HeadBranch
|
||||||
|
} else if pull.HeadRepo == nil {
|
||||||
|
ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
|
||||||
|
} else {
|
||||||
|
ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
|
||||||
|
}
|
||||||
|
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||||
|
ctx.Data["NumCommits"] = 0
|
||||||
|
ctx.Data["NumFiles"] = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commitStatuses, err := models.GetLatestCommitStatus(repo, sha, 0)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetLatestCommitStatus", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(commitStatuses) > 0 {
|
||||||
|
ctx.Data["LatestCommitStatuses"] = commitStatuses
|
||||||
|
ctx.Data["LatestCommitStatus"] = models.CalcCommitStatus(commitStatuses)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pull.ProtectedBranch != nil && pull.ProtectedBranch.EnableStatusCheck {
|
||||||
|
ctx.Data["is_context_required"] = func(context string) bool {
|
||||||
|
for _, c := range pull.ProtectedBranch.StatusCheckContexts {
|
||||||
|
if c == context {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pull.ProtectedBranch.StatusCheckContexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
|
||||||
|
ctx.Data["HeadBranchCommitID"] = headBranchSha
|
||||||
|
ctx.Data["PullHeadCommitID"] = sha
|
||||||
|
|
||||||
|
if pull.HeadRepo == nil || !headBranchExist || headBranchSha != sha {
|
||||||
|
ctx.Data["IsPullRequestBroken"] = true
|
||||||
|
if pull.IsSameRepo() {
|
||||||
|
ctx.Data["HeadTarget"] = pull.HeadBranch
|
||||||
|
} else if pull.HeadRepo == nil {
|
||||||
|
ctx.Data["HeadTarget"] = "<deleted>:" + pull.HeadBranch
|
||||||
|
} else {
|
||||||
|
ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
|
||||||
|
git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName())
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||||
|
ctx.Data["IsPullRequestBroken"] = true
|
||||||
|
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||||
|
ctx.Data["NumCommits"] = 0
|
||||||
|
ctx.Data["NumFiles"] = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ServerError("GetCompareInfo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if pull.IsWorkInProgress() {
|
||||||
|
ctx.Data["IsPullWorkInProgress"] = true
|
||||||
|
ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix()
|
||||||
|
}
|
||||||
|
|
||||||
|
if pull.IsFilesConflicted() {
|
||||||
|
ctx.Data["IsPullFilesConflicted"] = true
|
||||||
|
ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["NumCommits"] = compareInfo.Commits.Len()
|
||||||
|
ctx.Data["NumFiles"] = compareInfo.NumFiles
|
||||||
|
return compareInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPullInfo(ctx *context.Context) *models.Issue {
|
||||||
|
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||||
|
if err != nil {
|
||||||
|
// if models.IsErrIssueNotExist(err) {
|
||||||
|
// ctx.NotFound("GetIssueByIndex", err)
|
||||||
|
// } else {
|
||||||
|
// ctx.ServerError("GetIssueByIndex", err)
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err = issue.LoadPoster(); err != nil {
|
||||||
|
// ctx.ServerError("LoadPoster", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := issue.LoadRepo(); err != nil {
|
||||||
|
// ctx.ServerError("LoadRepo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
|
||||||
|
ctx.Data["Issue"] = issue
|
||||||
|
|
||||||
|
if !issue.IsPull {
|
||||||
|
// ctx.NotFound("ViewPullCommits", nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = issue.LoadPullRequest(); err != nil {
|
||||||
|
// ctx.ServerError("LoadPullRequest", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = issue.PullRequest.LoadHeadRepo(); err != nil {
|
||||||
|
// ctx.ServerError("LoadHeadRepo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsSigned {
|
||||||
|
// Update issue-user.
|
||||||
|
if err = issue.ReadBy(ctx.User.ID); err != nil {
|
||||||
|
// ctx.ServerError("ReadBy", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrepareMergedViewPullInfo(ctx *context.Context, issue *models.Issue) *git.CompareInfo {
|
||||||
|
pull := issue.PullRequest
|
||||||
|
|
||||||
|
setMergeTarget(ctx, pull)
|
||||||
|
ctx.Data["HasMerged"] = true
|
||||||
|
|
||||||
|
compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
|
||||||
|
pull.MergeBase, pull.GetGitRefName())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "fatal: Not a valid object name") {
|
||||||
|
ctx.Data["IsPullRequestBroken"] = true
|
||||||
|
ctx.Data["BaseTarget"] = "deleted"
|
||||||
|
ctx.Data["NumCommits"] = 0
|
||||||
|
ctx.Data["NumFiles"] = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.ServerError("GetCompareInfo", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["NumCommits"] = compareInfo.Commits.Len()
|
||||||
|
ctx.Data["NumFiles"] = compareInfo.NumFiles
|
||||||
|
return compareInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMergeTarget(ctx *context.Context, pull *models.PullRequest) {
|
||||||
|
if ctx.Repo.Owner.Name == pull.MustHeadUserName() {
|
||||||
|
ctx.Data["HeadTarget"] = pull.HeadBranch
|
||||||
|
} else if pull.HeadRepo == nil {
|
||||||
|
ctx.Data["HeadTarget"] = pull.MustHeadUserName() + ":" + pull.HeadBranch
|
||||||
|
} else {
|
||||||
|
ctx.Data["HeadTarget"] = pull.MustHeadUserName() + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
|
||||||
|
}
|
||||||
|
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||||
|
}
|
||||||
|
|
||||||
|
// end by qiubing
|
||||||
|
|
||||||
// DownloadPullDiff render a pull's raw diff
|
// DownloadPullDiff render a pull's raw diff
|
||||||
func DownloadPullDiff(ctx *context.APIContext) {
|
func DownloadPullDiff(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.diff repository repoDownloadPullDiff
|
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}.diff repository repoDownloadPullDiff
|
||||||
|
|
|
@ -800,3 +800,437 @@ func Delete(ctx *context.APIContext) {
|
||||||
log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
|
log.Trace("Repository deleted: %s/%s", owner.Name, repo.Name)
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add by qiubing
|
||||||
|
// MustBeNotEmpty render when a repo is a empty git dir
|
||||||
|
func MustBeNotEmpty(ctx *context.Context) {
|
||||||
|
if ctx.Repo.Repository.IsEmpty {
|
||||||
|
ctx.NotFound("MustBeNotEmpty", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEditorconfigIfExists set editor config as render variable
|
||||||
|
func SetEditorconfigIfExists(ctx *context.Context) {
|
||||||
|
if ctx.Repo.Repository.IsEmpty {
|
||||||
|
ctx.Data["Editorconfig"] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ec, err := ctx.Repo.GetEditorconfig()
|
||||||
|
|
||||||
|
if err != nil && !git.IsErrNotExist(err) {
|
||||||
|
description := fmt.Sprintf("Error while getting .editorconfig file: %v", err)
|
||||||
|
if err := models.CreateRepositoryNotice(description); err != nil {
|
||||||
|
ctx.ServerError("ErrCreatingReporitoryNotice", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Editorconfig"] = ec
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDiffViewStyle set diff style as render variable
|
||||||
|
func SetDiffViewStyle(ctx *context.Context) {
|
||||||
|
queryStyle := ctx.Query("style")
|
||||||
|
|
||||||
|
if !ctx.IsSigned {
|
||||||
|
ctx.Data["IsSplitStyle"] = queryStyle == "split"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
userStyle = ctx.User.DiffViewStyle
|
||||||
|
style string
|
||||||
|
)
|
||||||
|
|
||||||
|
if queryStyle == "unified" || queryStyle == "split" {
|
||||||
|
style = queryStyle
|
||||||
|
} else if userStyle == "unified" || userStyle == "split" {
|
||||||
|
style = userStyle
|
||||||
|
} else {
|
||||||
|
style = "unified"
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["IsSplitStyle"] = style == "split"
|
||||||
|
if err := ctx.User.UpdateDiffViewStyle(style); err != nil {
|
||||||
|
ctx.ServerError("ErrUpdateDiffViewStyle", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CompareDiff(ctx *context.APIContext) {
|
||||||
|
_ = "df"
|
||||||
|
_, _, headGitRepo, compareInfo, _, _ := ParseCompareInfo(ctx.Context)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer headGitRepo.Close()
|
||||||
|
|
||||||
|
result := make([]*git.Commit, 0)
|
||||||
|
for commit := compareInfo.Commits.Front(); commit != nil; commit = commit.Next() {
|
||||||
|
temp := commit.Value.(*git.Commit)
|
||||||
|
result = append(result, temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(200, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseCompareInfo parse compare info between two commit for preparing comparing references
|
||||||
|
func ParseCompareInfo(ctx *context.Context) (*models.User, *models.Repository, *git.Repository, *git.CompareInfo, string, string) {
|
||||||
|
baseRepo := ctx.Repo.Repository
|
||||||
|
|
||||||
|
// Get compared branches information
|
||||||
|
// A full compare url is of the form:
|
||||||
|
//
|
||||||
|
// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
|
||||||
|
// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
|
||||||
|
// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
|
||||||
|
//
|
||||||
|
// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
|
||||||
|
// with the :baseRepo in ctx.Repo.
|
||||||
|
//
|
||||||
|
// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
|
||||||
|
//
|
||||||
|
// How do we determine the :headRepo?
|
||||||
|
//
|
||||||
|
// 1. If :headOwner is not set then the :headRepo = :baseRepo
|
||||||
|
// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
|
||||||
|
// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
|
||||||
|
// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
|
||||||
|
//
|
||||||
|
// format: <base branch>...[<head repo>:]<head branch>
|
||||||
|
// base<-head: master...head:feature
|
||||||
|
// same repo: master...feature
|
||||||
|
|
||||||
|
var (
|
||||||
|
headUser *models.User
|
||||||
|
headRepo *models.Repository
|
||||||
|
headBranch string
|
||||||
|
isSameRepo bool
|
||||||
|
infoPath string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
infoPath = ctx.Params("*")
|
||||||
|
infos := strings.SplitN(infoPath, "...", 2)
|
||||||
|
if len(infos) != 2 {
|
||||||
|
log.Trace("ParseCompareInfo[%d]: not enough compared branches information %s", baseRepo.ID, infos)
|
||||||
|
ctx.NotFound("CompareAndPullRequest", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["BaseName"] = baseRepo.OwnerName
|
||||||
|
baseBranch := infos[0]
|
||||||
|
ctx.Data["BaseBranch"] = baseBranch
|
||||||
|
|
||||||
|
// If there is no head repository, it means compare between same repository.
|
||||||
|
headInfos := strings.Split(infos[1], ":")
|
||||||
|
if len(headInfos) == 1 {
|
||||||
|
isSameRepo = true
|
||||||
|
headUser = ctx.Repo.Owner
|
||||||
|
headBranch = headInfos[0]
|
||||||
|
|
||||||
|
} else if len(headInfos) == 2 {
|
||||||
|
headInfosSplit := strings.Split(headInfos[0], "/")
|
||||||
|
if len(headInfosSplit) == 1 {
|
||||||
|
headUser, err = models.GetUserByName(headInfos[0])
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrUserNotExist(err) {
|
||||||
|
ctx.NotFound("GetUserByName", nil)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetUserByName", err)
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
headBranch = headInfos[1]
|
||||||
|
isSameRepo = headUser.ID == ctx.Repo.Owner.ID
|
||||||
|
if isSameRepo {
|
||||||
|
headRepo = baseRepo
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headRepo, err = models.GetRepositoryByOwnerAndName(headInfosSplit[0], headInfosSplit[1])
|
||||||
|
if err != nil {
|
||||||
|
if models.IsErrRepoNotExist(err) {
|
||||||
|
ctx.NotFound("GetRepositoryByOwnerAndName", nil)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetRepositoryByOwnerAndName", err)
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
if err := headRepo.GetOwner(); err != nil {
|
||||||
|
if models.IsErrUserNotExist(err) {
|
||||||
|
ctx.NotFound("GetUserByName", nil)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetUserByName", err)
|
||||||
|
}
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
headBranch = headInfos[1]
|
||||||
|
headUser = headRepo.Owner
|
||||||
|
isSameRepo = headRepo.ID == ctx.Repo.Repository.ID
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.NotFound("CompareAndPullRequest", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
ctx.Data["HeadUser"] = headUser
|
||||||
|
ctx.Data["HeadBranch"] = headBranch
|
||||||
|
ctx.Repo.PullRequest.SameRepo = isSameRepo
|
||||||
|
|
||||||
|
// Check if base branch is valid.
|
||||||
|
baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(baseBranch)
|
||||||
|
baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(baseBranch)
|
||||||
|
baseIsTag := ctx.Repo.GitRepo.IsTagExist(baseBranch)
|
||||||
|
if !baseIsCommit && !baseIsBranch && !baseIsTag {
|
||||||
|
// Check if baseBranch is short sha commit hash
|
||||||
|
if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(baseBranch); baseCommit != nil {
|
||||||
|
baseBranch = baseCommit.ID.String()
|
||||||
|
ctx.Data["BaseBranch"] = baseBranch
|
||||||
|
baseIsCommit = true
|
||||||
|
} else {
|
||||||
|
ctx.NotFound("IsRefExist", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Data["BaseIsCommit"] = baseIsCommit
|
||||||
|
ctx.Data["BaseIsBranch"] = baseIsBranch
|
||||||
|
ctx.Data["BaseIsTag"] = baseIsTag
|
||||||
|
|
||||||
|
// Now we have the repository that represents the base
|
||||||
|
|
||||||
|
// The current base and head repositories and branches may not
|
||||||
|
// actually be the intended branches that the user wants to
|
||||||
|
// create a pull-request from - but also determining the head
|
||||||
|
// repo is difficult.
|
||||||
|
|
||||||
|
// We will want therefore to offer a few repositories to set as
|
||||||
|
// our base and head
|
||||||
|
|
||||||
|
// 1. First if the baseRepo is a fork get the "RootRepo" it was
|
||||||
|
// forked from
|
||||||
|
var rootRepo *models.Repository
|
||||||
|
if baseRepo.IsFork {
|
||||||
|
err = baseRepo.GetBaseRepo()
|
||||||
|
if err != nil {
|
||||||
|
if !models.IsErrRepoNotExist(err) {
|
||||||
|
ctx.ServerError("Unable to find root repo", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rootRepo = baseRepo.BaseRepo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Now if the current user is not the owner of the baseRepo,
|
||||||
|
// check if they have a fork of the base repo and offer that as
|
||||||
|
// "OwnForkRepo"
|
||||||
|
var ownForkRepo *models.Repository
|
||||||
|
if ctx.User != nil && baseRepo.OwnerID != ctx.User.ID {
|
||||||
|
repo, has := models.HasForkedRepo(ctx.User.ID, baseRepo.ID)
|
||||||
|
if has {
|
||||||
|
ownForkRepo = repo
|
||||||
|
ctx.Data["OwnForkRepo"] = ownForkRepo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has := headRepo != nil
|
||||||
|
// 3. If the base is a forked from "RootRepo" and the owner of
|
||||||
|
// the "RootRepo" is the :headUser - set headRepo to that
|
||||||
|
if !has && rootRepo != nil && rootRepo.OwnerID == headUser.ID {
|
||||||
|
headRepo = rootRepo
|
||||||
|
has = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. If the ctx.User has their own fork of the baseRepo and the headUser is the ctx.User
|
||||||
|
// set the headRepo to the ownFork
|
||||||
|
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headUser.ID {
|
||||||
|
headRepo = ownForkRepo
|
||||||
|
has = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. If the headOwner has a fork of the baseRepo - use that
|
||||||
|
if !has {
|
||||||
|
headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
|
||||||
|
if !has && baseRepo.IsFork {
|
||||||
|
headRepo, has = models.HasForkedRepo(headUser.ID, baseRepo.ForkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Otherwise if we're not the same repo and haven't found a repo give up
|
||||||
|
if !isSameRepo && !has {
|
||||||
|
ctx.Data["PageIsComparePull"] = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Finally open the git repo
|
||||||
|
var headGitRepo *git.Repository
|
||||||
|
if isSameRepo {
|
||||||
|
headRepo = ctx.Repo.Repository
|
||||||
|
headGitRepo = ctx.Repo.GitRepo
|
||||||
|
} else if has {
|
||||||
|
headGitRepo, err = git.OpenRepository(headRepo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("OpenRepository", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
defer headGitRepo.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["HeadRepo"] = headRepo
|
||||||
|
|
||||||
|
// Now we need to assert that the ctx.User has permission to read
|
||||||
|
// the baseRepo's code and pulls
|
||||||
|
// (NOT headRepo's)
|
||||||
|
permBase, err := models.GetUserRepoPermission(baseRepo, ctx.User)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
if !permBase.CanRead(models.UnitTypeCode) {
|
||||||
|
if log.IsTrace() {
|
||||||
|
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
|
||||||
|
ctx.User,
|
||||||
|
baseRepo,
|
||||||
|
permBase)
|
||||||
|
}
|
||||||
|
ctx.NotFound("ParseCompareInfo", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not merging from the same repo:
|
||||||
|
if !isSameRepo {
|
||||||
|
// Assert ctx.User has permission to read headRepo's codes
|
||||||
|
permHead, err := models.GetUserRepoPermission(headRepo, ctx.User)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
if !permHead.CanRead(models.UnitTypeCode) {
|
||||||
|
if log.IsTrace() {
|
||||||
|
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
|
||||||
|
ctx.User,
|
||||||
|
headRepo,
|
||||||
|
permHead)
|
||||||
|
}
|
||||||
|
ctx.NotFound("ParseCompareInfo", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a rootRepo and it's different from:
|
||||||
|
// 1. the computed base
|
||||||
|
// 2. the computed head
|
||||||
|
// then get the branches of it
|
||||||
|
if rootRepo != nil &&
|
||||||
|
rootRepo.ID != headRepo.ID &&
|
||||||
|
rootRepo.ID != baseRepo.ID {
|
||||||
|
perm, branches, err := getBranchesForRepo(ctx.User, rootRepo)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBranchesForRepo", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
if perm {
|
||||||
|
ctx.Data["RootRepo"] = rootRepo
|
||||||
|
ctx.Data["RootRepoBranches"] = branches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a ownForkRepo and it's different from:
|
||||||
|
// 1. The computed base
|
||||||
|
// 2. The computed hea
|
||||||
|
// 3. The rootRepo (if we have one)
|
||||||
|
// then get the branches from it.
|
||||||
|
if ownForkRepo != nil &&
|
||||||
|
ownForkRepo.ID != headRepo.ID &&
|
||||||
|
ownForkRepo.ID != baseRepo.ID &&
|
||||||
|
(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
|
||||||
|
perm, branches, err := getBranchesForRepo(ctx.User, ownForkRepo)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBranchesForRepo", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
if perm {
|
||||||
|
ctx.Data["OwnForkRepo"] = ownForkRepo
|
||||||
|
ctx.Data["OwnForkRepoBranches"] = branches
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if head branch is valid.
|
||||||
|
headIsCommit := headGitRepo.IsCommitExist(headBranch)
|
||||||
|
headIsBranch := headGitRepo.IsBranchExist(headBranch)
|
||||||
|
headIsTag := headGitRepo.IsTagExist(headBranch)
|
||||||
|
if !headIsCommit && !headIsBranch && !headIsTag {
|
||||||
|
// Check if headBranch is short sha commit hash
|
||||||
|
if headCommit, _ := headGitRepo.GetCommit(headBranch); headCommit != nil {
|
||||||
|
headBranch = headCommit.ID.String()
|
||||||
|
ctx.Data["HeadBranch"] = headBranch
|
||||||
|
headIsCommit = true
|
||||||
|
} else {
|
||||||
|
ctx.NotFound("IsRefExist", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Data["HeadIsCommit"] = headIsCommit
|
||||||
|
ctx.Data["HeadIsBranch"] = headIsBranch
|
||||||
|
ctx.Data["HeadIsTag"] = headIsTag
|
||||||
|
|
||||||
|
// Treat as pull request if both references are branches
|
||||||
|
if ctx.Data["PageIsComparePull"] == nil {
|
||||||
|
ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
|
||||||
|
if log.IsTrace() {
|
||||||
|
log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
|
||||||
|
ctx.User,
|
||||||
|
baseRepo,
|
||||||
|
permBase)
|
||||||
|
}
|
||||||
|
ctx.NotFound("ParseCompareInfo", nil)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
baseBranchRef := baseBranch
|
||||||
|
if baseIsBranch {
|
||||||
|
baseBranchRef = git.BranchPrefix + baseBranch
|
||||||
|
} else if baseIsTag {
|
||||||
|
baseBranchRef = git.TagPrefix + baseBranch
|
||||||
|
}
|
||||||
|
headBranchRef := headBranch
|
||||||
|
if headIsBranch {
|
||||||
|
headBranchRef = git.BranchPrefix + headBranch
|
||||||
|
} else if headIsTag {
|
||||||
|
headBranchRef = git.TagPrefix + headBranch
|
||||||
|
}
|
||||||
|
|
||||||
|
compareInfo, err := headGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetCompareInfo", err)
|
||||||
|
return nil, nil, nil, nil, "", ""
|
||||||
|
}
|
||||||
|
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
|
||||||
|
|
||||||
|
return headUser, headRepo, headGitRepo, compareInfo, baseBranch, headBranch
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBranchesForRepo(user *models.User, repo *models.Repository) (bool, []string, error) {
|
||||||
|
perm, err := models.GetUserRepoPermission(repo, user)
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
if !perm.CanRead(models.UnitTypeCode) {
|
||||||
|
return false, nil, nil
|
||||||
|
}
|
||||||
|
gitRepo, err := git.OpenRepository(repo.RepoPath())
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
branches, err := gitRepo.GetBranches()
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return true, branches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end by qiubing
|
||||||
|
|
Loading…
Reference in New Issue