diff options
author | zhsso <zhssoge@gmail.com> | 2014-04-10 14:20:58 -0400 |
---|---|---|
committer | zhsso <zhssoge@gmail.com> | 2014-04-10 14:20:58 -0400 |
commit | a4cbe79567072befd96cf1b7eb319de1e2809ca3 (patch) | |
tree | 3dff34e53f34632532fd7a05e00e6f06b3e7fb82 /models | |
parent | f3ed11d177d76bcb1850c6670c1516d25a66eb2c (diff) | |
download | gitea-a4cbe79567072befd96cf1b7eb319de1e2809ca3.tar.gz gitea-a4cbe79567072befd96cf1b7eb319de1e2809ca3.zip |
fix
Diffstat (limited to 'models')
-rw-r--r-- | models/access.go | 62 | ||||
-rw-r--r-- | models/action.go | 135 | ||||
-rw-r--r-- | models/git.go | 411 | ||||
-rw-r--r-- | models/issue.go | 234 | ||||
-rw-r--r-- | models/models.go | 131 | ||||
-rw-r--r-- | models/models_test.go | 55 | ||||
-rw-r--r-- | models/oauth2.go | 49 | ||||
-rw-r--r-- | models/publickey.go | 228 | ||||
-rw-r--r-- | models/repo.go | 647 | ||||
-rw-r--r-- | models/user.go | 479 |
10 files changed, 2431 insertions, 0 deletions
diff --git a/models/access.go b/models/access.go new file mode 100644 index 0000000000..2c0900151f --- /dev/null +++ b/models/access.go @@ -0,0 +1,62 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "strings" + "time" + + "github.com/lunny/xorm" +) + +// Access types. +const ( + AU_READABLE = iota + 1 + AU_WRITABLE +) + +// Access represents the accessibility of user to repository. +type Access struct { + Id int64 + UserName string `xorm:"unique(s)"` + RepoName string `xorm:"unique(s)"` + Mode int `xorm:"unique(s)"` + Created time.Time `xorm:"created"` +} + +// AddAccess adds new access record. +func AddAccess(access *Access) error { + access.UserName = strings.ToLower(access.UserName) + access.RepoName = strings.ToLower(access.RepoName) + _, err := orm.Insert(access) + return err +} + +// UpdateAccess updates access information. +func UpdateAccess(access *Access) error { + access.UserName = strings.ToLower(access.UserName) + access.RepoName = strings.ToLower(access.RepoName) + _, err := orm.Id(access.Id).Update(access) + return err +} + +// UpdateAccess updates access information with session for rolling back. +func UpdateAccessWithSession(sess *xorm.Session, access *Access) error { + if _, err := sess.Id(access.Id).Update(access); err != nil { + sess.Rollback() + return err + } + return nil +} + +// HasAccess returns true if someone can read or write to given repository. +func HasAccess(userName, repoName string, mode int) (bool, error) { + return orm.Get(&Access{ + Id: 0, + UserName: strings.ToLower(userName), + RepoName: strings.ToLower(repoName), + Mode: mode, + }) +} diff --git a/models/action.go b/models/action.go new file mode 100644 index 0000000000..a642a82c98 --- /dev/null +++ b/models/action.go @@ -0,0 +1,135 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "encoding/json" + "time" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" +) + +// Operation types of user action. +const ( + OP_CREATE_REPO = iota + 1 + OP_DELETE_REPO + OP_STAR_REPO + OP_FOLLOW_REPO + OP_COMMIT_REPO + OP_CREATE_ISSUE + OP_PULL_REQUEST + OP_TRANSFER_REPO +) + +// Action represents user operation type and other information to repository., +// it implemented interface base.Actioner so that can be used in template render. +type Action struct { + Id int64 + UserId int64 // Receiver user id. + OpType int // Operations: CREATE DELETE STAR ... + ActUserId int64 // Action user id. + ActUserName string // Action user name. + ActEmail string + RepoId int64 + RepoName string + RefName string + Content string `xorm:"TEXT"` + Created time.Time `xorm:"created"` +} + +func (a Action) GetOpType() int { + return a.OpType +} + +func (a Action) GetActUserName() string { + return a.ActUserName +} + +func (a Action) GetActEmail() string { + return a.ActEmail +} + +func (a Action) GetRepoName() string { + return a.RepoName +} + +func (a Action) GetBranch() string { + return a.RefName +} + +func (a Action) GetContent() string { + return a.Content +} + +// CommitRepoAction adds new action for committing repository. +func CommitRepoAction(userId int64, userName, actEmail string, + repoId int64, repoName string, refName string, commit *base.PushCommits) error { + log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName) + + bs, err := json.Marshal(commit) + if err != nil { + log.Error("action.CommitRepoAction(json): %d/%s", userId, repoName) + return err + } + + if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail, + OpType: OP_COMMIT_REPO, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil { + log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName) + return err + } + + // Change repository bare status and update last updated time. + repo, err := GetRepositoryByName(userId, repoName) + if err != nil { + log.Error("action.CommitRepoAction(GetRepositoryByName): %d/%s", userId, repoName) + return err + } + repo.IsBare = false + if err = UpdateRepository(repo); err != nil { + log.Error("action.CommitRepoAction(UpdateRepository): %d/%s", userId, repoName) + return err + } + + log.Trace("action.CommitRepoAction(end): %d/%s", userId, repoName) + return nil +} + +// NewRepoAction adds new action for creating repository. +func NewRepoAction(user *User, repo *Repository) (err error) { + if err = NotifyWatchers(&Action{ActUserId: user.Id, ActUserName: user.Name, ActEmail: user.Email, + OpType: OP_CREATE_REPO, RepoId: repo.Id, RepoName: repo.Name}); err != nil { + log.Error("action.NewRepoAction(notify watchers): %d/%s", user.Id, repo.Name) + return err + } + + log.Trace("action.NewRepoAction: %s/%s", user.LowerName, repo.LowerName) + return err +} + +// TransferRepoAction adds new action for transfering repository. +func TransferRepoAction(user, newUser *User, repo *Repository) (err error) { + if err = NotifyWatchers(&Action{ActUserId: user.Id, ActUserName: user.Name, ActEmail: user.Email, + OpType: OP_TRANSFER_REPO, RepoId: repo.Id, RepoName: repo.Name, Content: newUser.Name}); err != nil { + log.Error("action.TransferRepoAction(notify watchers): %d/%s", user.Id, repo.Name) + return err + } + + log.Trace("action.TransferRepoAction: %s/%s", user.LowerName, repo.LowerName) + return err +} + +// GetFeeds returns action list of given user in given context. +func GetFeeds(userid, offset int64, isProfile bool) ([]Action, error) { + actions := make([]Action, 0, 20) + sess := orm.Limit(20, int(offset)).Desc("id").Where("user_id=?", userid) + if isProfile { + sess.And("act_user_id=?", userid) + } else { + sess.And("act_user_id!=?", userid) + } + err := sess.Find(&actions) + return actions, err +} diff --git a/models/git.go b/models/git.go new file mode 100644 index 0000000000..77b7ef2d7e --- /dev/null +++ b/models/git.go @@ -0,0 +1,411 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "bufio" + "container/list" + "fmt" + "io" + "os" + "os/exec" + "path" + "strings" + + "github.com/gogits/git" + + "github.com/gogits/gogs/modules/base" +) + +// RepoFile represents a file object in git repository. +type RepoFile struct { + *git.TreeEntry + Path string + Size int64 + Repo *git.Repository + Commit *git.Commit +} + +// LookupBlob returns the content of an object. +func (file *RepoFile) LookupBlob() (*git.Blob, error) { + if file.Repo == nil { + return nil, ErrRepoFileNotLoaded + } + + return file.Repo.LookupBlob(file.Id) +} + +// GetBranches returns all branches of given repository. +func GetBranches(userName, repoName string) ([]string, error) { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return nil, err + } + + refs, err := repo.AllReferences() + if err != nil { + return nil, err + } + + brs := make([]string, len(refs)) + for i, ref := range refs { + brs[i] = ref.BranchName() + } + return brs, nil +} + +// GetTags returns all tags of given repository. +func GetTags(userName, repoName string) ([]string, error) { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return nil, err + } + + refs, err := repo.AllTags() + if err != nil { + return nil, err + } + + tags := make([]string, len(refs)) + for i, ref := range refs { + tags[i] = ref.Name + } + return tags, nil +} + +func IsBranchExist(userName, repoName, branchName string) bool { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return false + } + return repo.IsBranchExist(branchName) +} + +func GetTargetFile(userName, repoName, branchName, commitId, rpath string) (*RepoFile, error) { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return nil, err + } + + commit, err := repo.GetCommitOfBranch(branchName) + if err != nil { + commit, err = repo.GetCommit(commitId) + if err != nil { + return nil, err + } + } + + parts := strings.Split(path.Clean(rpath), "/") + + var entry *git.TreeEntry + tree := commit.Tree + for i, part := range parts { + if i == len(parts)-1 { + entry = tree.EntryByName(part) + if entry == nil { + return nil, ErrRepoFileNotExist + } + } else { + tree, err = repo.SubTree(tree, part) + if err != nil { + return nil, err + } + } + } + + size, err := repo.ObjectSize(entry.Id) + if err != nil { + return nil, err + } + + repoFile := &RepoFile{ + entry, + rpath, + size, + repo, + commit, + } + + return repoFile, nil +} + +// GetReposFiles returns a list of file object in given directory of repository. +// func GetReposFilesOfBranch(userName, repoName, branchName, rpath string) ([]*RepoFile, error) { +// return getReposFiles(userName, repoName, commitId, rpath) +// } + +// GetReposFiles returns a list of file object in given directory of repository. +func GetReposFiles(userName, repoName, commitId, rpath string) ([]*RepoFile, error) { + return getReposFiles(userName, repoName, commitId, rpath) +} + +func getReposFiles(userName, repoName, commitId string, rpath string) ([]*RepoFile, error) { + repopath := RepoPath(userName, repoName) + repo, err := git.OpenRepository(repopath) + if err != nil { + return nil, err + } + + commit, err := repo.GetCommit(commitId) + if err != nil { + return nil, err + } + + var repodirs []*RepoFile + var repofiles []*RepoFile + commit.Tree.Walk(func(dirname string, entry *git.TreeEntry) int { + if dirname == rpath { + // TODO: size get method shoule be improved + size, err := repo.ObjectSize(entry.Id) + if err != nil { + return 0 + } + + cmd := exec.Command("git", "log", "-1", "--pretty=format:%H", commitId, "--", path.Join(dirname, entry.Name)) + cmd.Dir = repopath + out, err := cmd.Output() + if err != nil { + return 0 + } + filecm, err := repo.GetCommit(string(out)) + if err != nil { + return 0 + } + + rp := &RepoFile{ + entry, + path.Join(dirname, entry.Name), + size, + repo, + filecm, + } + + if entry.IsFile() { + repofiles = append(repofiles, rp) + } else if entry.IsDir() { + repodirs = append(repodirs, rp) + } + } + return 0 + }) + + return append(repodirs, repofiles...), nil +} + +func GetCommit(userName, repoName, commitId string) (*git.Commit, error) { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return nil, err + } + + return repo.GetCommit(commitId) +} + +// GetCommitsByBranch returns all commits of given branch of repository. +func GetCommitsByBranch(userName, repoName, branchName string) (*list.List, error) { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return nil, err + } + r, err := repo.LookupReference(fmt.Sprintf("refs/heads/%s", branchName)) + if err != nil { + return nil, err + } + return r.AllCommits() +} + +// GetCommitsByCommitId returns all commits of given commitId of repository. +func GetCommitsByCommitId(userName, repoName, commitId string) (*list.List, error) { + repo, err := git.OpenRepository(RepoPath(userName, repoName)) + if err != nil { + return nil, err + } + oid, err := git.NewOidFromString(commitId) + if err != nil { + return nil, err + } + return repo.CommitsBefore(oid) +} + +// Diff line types. +const ( + DIFF_LINE_PLAIN = iota + 1 + DIFF_LINE_ADD + DIFF_LINE_DEL + DIFF_LINE_SECTION +) + +const ( + DIFF_FILE_ADD = iota + 1 + DIFF_FILE_CHANGE + DIFF_FILE_DEL +) + +type DiffLine struct { + LeftIdx int + RightIdx int + Type int + Content string +} + +func (d DiffLine) GetType() int { + return d.Type +} + +type DiffSection struct { + Name string + Lines []*DiffLine +} + +type DiffFile struct { + Name string + Addition, Deletion int + Type int + Sections []*DiffSection +} + +type Diff struct { + TotalAddition, TotalDeletion int + Files []*DiffFile +} + +func (diff *Diff) NumFiles() int { + return len(diff.Files) +} + +const DIFF_HEAD = "diff --git " + +func ParsePatch(reader io.Reader) (*Diff, error) { + scanner := bufio.NewScanner(reader) + var ( + curFile *DiffFile + curSection = &DiffSection{ + Lines: make([]*DiffLine, 0, 10), + } + + leftLine, rightLine int + ) + + diff := &Diff{Files: make([]*DiffFile, 0)} + var i int + for scanner.Scan() { + line := scanner.Text() + // fmt.Println(i, line) + if strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- ") { + continue + } + + i = i + 1 + if line == "" { + continue + } + if line[0] == ' ' { + diffLine := &DiffLine{Type: DIFF_LINE_PLAIN, Content: line, LeftIdx: leftLine, RightIdx: rightLine} + leftLine++ + rightLine++ + curSection.Lines = append(curSection.Lines, diffLine) + continue + } else if line[0] == '@' { + curSection = &DiffSection{} + curFile.Sections = append(curFile.Sections, curSection) + ss := strings.Split(line, "@@") + diffLine := &DiffLine{Type: DIFF_LINE_SECTION, Content: line} + curSection.Lines = append(curSection.Lines, diffLine) + + // Parse line number. + ranges := strings.Split(ss[len(ss)-2][1:], " ") + leftLine, _ = base.StrTo(strings.Split(ranges[0], ",")[0][1:]).Int() + rightLine, _ = base.StrTo(strings.Split(ranges[1], ",")[0]).Int() + continue + } else if line[0] == '+' { + curFile.Addition++ + diff.TotalAddition++ + diffLine := &DiffLine{Type: DIFF_LINE_ADD, Content: line, RightIdx: rightLine} + rightLine++ + curSection.Lines = append(curSection.Lines, diffLine) + continue + } else if line[0] == '-' { + curFile.Deletion++ + diff.TotalDeletion++ + diffLine := &DiffLine{Type: DIFF_LINE_DEL, Content: line, LeftIdx: leftLine} + if leftLine > 0 { + leftLine++ + } + curSection.Lines = append(curSection.Lines, diffLine) + continue + } + + // Get new file. + if strings.HasPrefix(line, DIFF_HEAD) { + fs := strings.Split(line[len(DIFF_HEAD):], " ") + a := fs[0] + + curFile = &DiffFile{ + Name: a[strings.Index(a, "/")+1:], + Type: DIFF_FILE_CHANGE, + Sections: make([]*DiffSection, 0, 10), + } + diff.Files = append(diff.Files, curFile) + + // Check file diff type. + for scanner.Scan() { + switch { + case strings.HasPrefix(scanner.Text(), "new file"): + curFile.Type = DIFF_FILE_ADD + case strings.HasPrefix(scanner.Text(), "deleted"): + curFile.Type = DIFF_FILE_DEL + case strings.HasPrefix(scanner.Text(), "index"): + curFile.Type = DIFF_FILE_CHANGE + } + if curFile.Type > 0 { + break + } + } + } + } + + return diff, nil +} + +func GetDiff(repoPath, commitid string) (*Diff, error) { + repo, err := git.OpenRepository(repoPath) + if err != nil { + return nil, err + } + + commit, err := repo.GetCommit(commitid) + if err != nil { + return nil, err + } + + // First commit of repository. + if commit.ParentCount() == 0 { + rd, wr := io.Pipe() + go func() { + cmd := exec.Command("git", "show", commitid) + cmd.Dir = repoPath + cmd.Stdout = wr + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Run() + wr.Close() + }() + defer rd.Close() + return ParsePatch(rd) + } + + rd, wr := io.Pipe() + go func() { + cmd := exec.Command("git", "diff", commit.Parent(0).Oid.String(), commitid) + cmd.Dir = repoPath + cmd.Stdout = wr + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + cmd.Run() + wr.Close() + }() + defer rd.Close() + return ParsePatch(rd) +} diff --git a/models/issue.go b/models/issue.go new file mode 100644 index 0000000000..f14030df5e --- /dev/null +++ b/models/issue.go @@ -0,0 +1,234 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "errors" + "strings" + "time" + + "github.com/gogits/gogs/modules/base" +) + +var ( + ErrIssueNotExist = errors.New("Issue does not exist") +) + +// Issue represents an issue or pull request of repository. +type Issue struct { + Id int64 + Index int64 // Index in one repository. + Name string + RepoId int64 `xorm:"index"` + Repo *Repository `xorm:"-"` + PosterId int64 + Poster *User `xorm:"-"` + MilestoneId int64 + AssigneeId int64 + IsPull bool // Indicates whether is a pull request or not. + IsClosed bool + Labels string `xorm:"TEXT"` + Mentions string `xorm:"TEXT"` + Content string `xorm:"TEXT"` + RenderedContent string `xorm:"-"` + NumComments int + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` +} + +// CreateIssue creates new issue for repository. +func CreateIssue(userId, repoId, milestoneId, assigneeId int64, issueCount int, name, labels, content string, isPull bool) (issue *Issue, err error) { + // TODO: find out mentions + mentions := "" + + sess := orm.NewSession() + defer sess.Close() + sess.Begin() + + issue = &Issue{ + Index: int64(issueCount) + 1, + Name: name, + RepoId: repoId, + PosterId: userId, + MilestoneId: milestoneId, + AssigneeId: assigneeId, + IsPull: isPull, + Labels: labels, + Mentions: mentions, + Content: content, + } + if _, err = sess.Insert(issue); err != nil { + sess.Rollback() + return nil, err + } + + rawSql := "UPDATE `repository` SET num_issues = num_issues + 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, repoId); err != nil { + sess.Rollback() + return nil, err + } + + if err = sess.Commit(); err != nil { + sess.Rollback() + return nil, err + } + + return issue, nil +} + +// GetIssueById returns issue object by given id. +func GetIssueByIndex(repoId, index int64) (*Issue, error) { + issue := &Issue{RepoId: repoId, Index: index} + has, err := orm.Get(issue) + if err != nil { + return nil, err + } else if !has { + return nil, ErrIssueNotExist + } + return issue, nil +} + +// GetIssues returns a list of issues by given conditions. +func GetIssues(userId, repoId, posterId, milestoneId int64, page int, isClosed, isMention bool, labels, sortType string) ([]Issue, error) { + sess := orm.Limit(20, (page-1)*20) + + if repoId > 0 { + sess.Where("repo_id=?", repoId).And("is_closed=?", isClosed) + } else { + sess.Where("is_closed=?", isClosed) + } + + if userId > 0 { + sess.And("assignee_id=?", userId) + } else if posterId > 0 { + sess.And("poster_id=?", posterId) + } else if isMention { + sess.And("mentions like '%$" + base.ToStr(userId) + "|%'") + } + + if milestoneId > 0 { + sess.And("milestone_id=?", milestoneId) + } + + if len(labels) > 0 { + for _, label := range strings.Split(labels, ",") { + sess.And("mentions like '%$" + label + "|%'") + } + } + + switch sortType { + case "oldest": + sess.Asc("created") + case "recentupdate": + sess.Desc("updated") + case "leastupdate": + sess.Asc("updated") + case "mostcomment": + sess.Desc("num_comments") + case "leastcomment": + sess.Asc("num_comments") + default: + sess.Desc("created") + } + + var issues []Issue + err := sess.Find(&issues) + return issues, err +} + +// GetUserIssueCount returns the number of issues that were created by given user in repository. +func GetUserIssueCount(userId, repoId int64) int64 { + count, _ := orm.Where("poster_id=?", userId).And("repo_id=?", repoId).Count(new(Issue)) + return count +} + +// UpdateIssue updates information of issue. +func UpdateIssue(issue *Issue) error { + _, err := orm.Id(issue.Id).AllCols().Update(issue) + return err +} + +// Label represents a list of labels of repository for issues. +type Label struct { + Id int64 + RepoId int64 `xorm:"index"` + Names string + Colors string +} + +// Milestone represents a milestone of repository. +type Milestone struct { + Id int64 + Name string + RepoId int64 `xorm:"index"` + IsClosed bool + Content string + NumIssues int + DueDate time.Time + Created time.Time `xorm:"created"` +} + +// Issue types. +const ( + IT_PLAIN = iota // Pure comment. + IT_REOPEN // Issue reopen status change prompt. + IT_CLOSE // Issue close status change prompt. +) + +// Comment represents a comment in commit and issue page. +type Comment struct { + Id int64 + Type int + PosterId int64 + Poster *User `xorm:"-"` + IssueId int64 + CommitId int64 + Line int64 + Content string + Created time.Time `xorm:"created"` +} + +// CreateComment creates comment of issue or commit. +func CreateComment(userId, repoId, issueId, commitId, line int64, cmtType int, content string) error { + sess := orm.NewSession() + defer sess.Close() + sess.Begin() + + if _, err := orm.Insert(&Comment{PosterId: userId, Type: cmtType, IssueId: issueId, + CommitId: commitId, Line: line, Content: content}); err != nil { + sess.Rollback() + return err + } + + // Check comment type. + switch cmtType { + case IT_PLAIN: + rawSql := "UPDATE `issue` SET num_comments = num_comments + 1 WHERE id = ?" + if _, err := sess.Exec(rawSql, issueId); err != nil { + sess.Rollback() + return err + } + case IT_REOPEN: + rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues - 1 WHERE id = ?" + if _, err := sess.Exec(rawSql, repoId); err != nil { + sess.Rollback() + return err + } + case IT_CLOSE: + rawSql := "UPDATE `repository` SET num_closed_issues = num_closed_issues + 1 WHERE id = ?" + if _, err := sess.Exec(rawSql, repoId); err != nil { + sess.Rollback() + return err + } + } + return sess.Commit() +} + +// GetIssueComments returns list of comment by given issue id. +func GetIssueComments(issueId int64) ([]Comment, error) { + comments := make([]Comment, 0, 10) + err := orm.Asc("created").Find(&comments, &Comment{IssueId: issueId}) + return comments, err +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000000..ee96207d10 --- /dev/null +++ b/models/models.go @@ -0,0 +1,131 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + "os" + "path" + + _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" + "github.com/lunny/xorm" + // _ "github.com/mattn/go-sqlite3" + + "github.com/gogits/gogs/modules/base" +) + +var ( + orm *xorm.Engine + tables []interface{} + + HasEngine bool + + DbCfg struct { + Type, Host, Name, User, Pwd, Path, SslMode string + } + + UseSQLite3 bool +) + +func init() { + tables = append(tables, new(User), new(PublicKey), new(Repository), new(Watch), + new(Action), new(Access), new(Issue), new(Comment), new(Oauth2)) +} + +func LoadModelsConfig() { + DbCfg.Type = base.Cfg.MustValue("database", "DB_TYPE") + if DbCfg.Type == "sqlite3" { + UseSQLite3 = true + } + DbCfg.Host = base.Cfg.MustValue("database", "HOST") + DbCfg.Name = base.Cfg.MustValue("database", "NAME") + DbCfg.User = base.Cfg.MustValue("database", "USER") + DbCfg.Pwd = base.Cfg.MustValue("database", "PASSWD") + DbCfg.SslMode = base.Cfg.MustValue("database", "SSL_MODE") + DbCfg.Path = base.Cfg.MustValue("database", "PATH", "data/gogs.db") +} + +func NewTestEngine(x *xorm.Engine) (err error) { + switch DbCfg.Type { + case "mysql": + x, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", + DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name)) + case "postgres": + x, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", + DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode)) + // case "sqlite3": + // os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) + // x, err = xorm.NewEngine("sqlite3", DbCfg.Path) + default: + return fmt.Errorf("Unknown database type: %s", DbCfg.Type) + } + if err != nil { + return fmt.Errorf("models.init(fail to conntect database): %v", err) + } + return x.Sync(tables...) +} + +func SetEngine() (err error) { + switch DbCfg.Type { + case "mysql": + orm, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8", + DbCfg.User, DbCfg.Pwd, DbCfg.Host, DbCfg.Name)) + case "postgres": + orm, err = xorm.NewEngine("postgres", fmt.Sprintf("user=%s password=%s dbname=%s sslmode=%s", + DbCfg.User, DbCfg.Pwd, DbCfg.Name, DbCfg.SslMode)) + case "sqlite3": + os.MkdirAll(path.Dir(DbCfg.Path), os.ModePerm) + orm, err = xorm.NewEngine("sqlite3", DbCfg.Path) + default: + return fmt.Errorf("Unknown database type: %s", DbCfg.Type) + } + if err != nil { + return fmt.Errorf("models.init(fail to conntect database): %v", err) + } + + // WARNNING: for serv command, MUST remove the output to os.stdout, + // so use log file to instead print to stdout. + execDir, _ := base.ExecDir() + logPath := execDir + "/log/xorm.log" + os.MkdirAll(path.Dir(logPath), os.ModePerm) + + f, err := os.Create(logPath) + if err != nil { + return fmt.Errorf("models.init(fail to create xorm.log): %v", err) + } + orm.Logger = f + + orm.ShowSQL = true + orm.ShowDebug = true + orm.ShowErr = true + return nil +} + +func NewEngine() (err error) { + if err = SetEngine(); err != nil { + return err + } + if err = orm.Sync(tables...); err != nil { + return fmt.Errorf("sync database struct error: %v\n", err) + } + return nil +} + +type Statistic struct { + Counter struct { + User, PublicKey, Repo, Watch, Action, Access int64 + } +} + +func GetStatistic() (stats Statistic) { + stats.Counter.User, _ = orm.Count(new(User)) + stats.Counter.PublicKey, _ = orm.Count(new(PublicKey)) + stats.Counter.Repo, _ = orm.Count(new(Repository)) + stats.Counter.Watch, _ = orm.Count(new(Watch)) + stats.Counter.Action, _ = orm.Count(new(Action)) + stats.Counter.Access, _ = orm.Count(new(Access)) + return +} diff --git a/models/models_test.go b/models/models_test.go new file mode 100644 index 0000000000..b808f41d21 --- /dev/null +++ b/models/models_test.go @@ -0,0 +1,55 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "fmt" + "testing" + + "github.com/lunny/xorm" + _ "github.com/mattn/go-sqlite3" + . "github.com/smartystreets/goconvey/convey" + + "github.com/gogits/gogs/modules/base" +) + +func init() { + var err error + orm, err = xorm.NewEngine("sqlite3", "./test.db") + if err != nil { + fmt.Println(err) + } + + orm.ShowSQL = true + orm.ShowDebug = true + + err = orm.Sync(&User{}, &Repository{}) + if err != nil { + fmt.Println(err) + } + + base.RepoRootPath = "test" +} + +func TestCreateRepository(t *testing.T) { + user := User{Id: 1, Name: "foobar", Type: UT_INDIVIDUAL} + _, err := CreateRepository(&user, "test", "", "", "test repo desc", false, false) + if err != nil { + t.Error(err) + } +} + +func TestDeleteRepository(t *testing.T) { + err := DeleteRepository(1, 1, "foobar") + if err != nil { + t.Error(err) + } +} + +func TestCommitRepoAction(t *testing.T) { + Convey("Create a commit repository action", t, func() { + + }) +} diff --git a/models/oauth2.go b/models/oauth2.go new file mode 100644 index 0000000000..45728b0d51 --- /dev/null +++ b/models/oauth2.go @@ -0,0 +1,49 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import "errors" + +// OT: Oauth2 Type +const ( + OT_GITHUB = iota + 1 + OT_GOOGLE + OT_TWITTER +) + +var ( + ErrOauth2RecordNotExists = errors.New("not exists oauth2 record") + ErrOauth2NotAssociatedWithUser = errors.New("not associated with user") +) + +type Oauth2 struct { + Id int64 + Uid int64 // userId + User *User `xorm:"-"` + Type int `xorm:"pk unique(oauth)"` // twitter,github,google... + Identity string `xorm:"pk unique(oauth)"` // id.. + Token string `xorm:"VARCHAR(200) not null"` +} + +func AddOauth2(oa *Oauth2) (err error) { + if _, err = orm.Insert(oa); err != nil { + return err + } + return nil +} + +func GetOauth2(identity string) (oa *Oauth2, err error) { + oa = &Oauth2{Identity: identity} + isExist, err := orm.Get(oa) + if err != nil { + return + } else if !isExist { + return nil, ErrOauth2RecordNotExists + } else if oa.Uid == 0 { + return oa, ErrOauth2NotAssociatedWithUser + } + oa.User, err = GetUserById(oa.Uid) + return oa, err +} diff --git a/models/publickey.go b/models/publickey.go new file mode 100644 index 0000000000..ed47ff209d --- /dev/null +++ b/models/publickey.go @@ -0,0 +1,228 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/Unknwon/com" + + "github.com/gogits/gogs/modules/log" +) + +const ( + // "### autogenerated by gitgos, DO NOT EDIT\n" + TPL_PUBLICK_KEY = `command="%s serv key-%d",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty %s` +) + +var ( + ErrKeyAlreadyExist = errors.New("Public key already exist") +) + +var sshOpLocker = sync.Mutex{} + +var ( + sshPath string + appPath string +) + +// exePath returns the executable path. +func exePath() (string, error) { + file, err := exec.LookPath(os.Args[0]) + if err != nil { + return "", err + } + return filepath.Abs(file) +} + +// homeDir returns the home directory of current user. +func homeDir() string { + home, err := com.HomeDir() + if err != nil { + return "/" + } + return home +} + +func init() { + var err error + + appPath, err = exePath() + if err != nil { + fmt.Printf("publickey.init(fail to get app path): %v\n", err) + os.Exit(2) + } + + // Determine and create .ssh path. + sshPath = filepath.Join(homeDir(), ".ssh") + if err = os.MkdirAll(sshPath, os.ModePerm); err != nil { + fmt.Printf("publickey.init(fail to create sshPath(%s)): %v\n", sshPath, err) + os.Exit(2) + } +} + +// PublicKey represents a SSH key of user. +type PublicKey struct { + Id int64 + OwnerId int64 `xorm:"unique(s) index not null"` + Name string `xorm:"unique(s) not null"` + Fingerprint string + Content string `xorm:"TEXT not null"` + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` +} + +// GenAuthorizedKey returns formatted public key string. +func GenAuthorizedKey(keyId int64, key string) string { + return fmt.Sprintf(TPL_PUBLICK_KEY+"\n", appPath, keyId, key) +} + +// AddPublicKey adds new public key to database and SSH key file. +func AddPublicKey(key *PublicKey) (err error) { + // Check if public key name has been used. + has, err := orm.Get(key) + if err != nil { + return err + } else if has { + return ErrKeyAlreadyExist + } + + // Calculate fingerprint. + tmpPath := strings.Replace(filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond()), + "id_rsa.pub"), "\\", "/", -1) + os.MkdirAll(path.Dir(tmpPath), os.ModePerm) + if err = ioutil.WriteFile(tmpPath, []byte(key.Content), os.ModePerm); err != nil { + return err + } + stdout, _, err := com.ExecCmd("ssh-keygen", "-l", "-f", tmpPath) + if err != nil { + return err + } else if len(stdout) < 2 { + return errors.New("Not enough output for calculating fingerprint") + } + key.Fingerprint = strings.Split(stdout, " ")[1] + + // Save SSH key. + if _, err = orm.Insert(key); err != nil { + return err + } + if err = SaveAuthorizedKeyFile(key); err != nil { + if _, err2 := orm.Delete(key); err2 != nil { + return err2 + } + return err + } + + return nil +} + +func rewriteAuthorizedKeys(key *PublicKey, p, tmpP string) error { + // Delete SSH key in SSH key file. + sshOpLocker.Lock() + defer sshOpLocker.Unlock() + + fr, err := os.Open(p) + if err != nil { + return err + } + defer fr.Close() + + fw, err := os.Create(tmpP) + if err != nil { + return err + } + defer fw.Close() + + buf := bufio.NewReader(fr) + for { + line, errRead := buf.ReadString('\n') + line = strings.TrimSpace(line) + + if errRead != nil { + if errRead != io.EOF { + return errRead + } + + // Reached end of file, if nothing to read then break, + // otherwise handle the last line. + if len(line) == 0 { + break + } + } + + // Found the line and copy rest of file. + if strings.Contains(line, fmt.Sprintf("key-%d", key.Id)) && strings.Contains(line, key.Content) { + continue + } + // Still finding the line, copy the line that currently read. + if _, err = fw.WriteString(line + "\n"); err != nil { + return err + } + + if errRead == io.EOF { + break + } + } + return nil +} + +// DeletePublicKey deletes SSH key information both in database and authorized_keys file. +func DeletePublicKey(key *PublicKey) (err error) { + // Delete SSH key in database. + has, err := orm.Id(key.Id).Get(key) + if err != nil { + return err + } else if !has { + return errors.New("Public key does not exist") + } + if _, err = orm.Delete(key); err != nil { + return err + } + + p := filepath.Join(sshPath, "authorized_keys") + tmpP := filepath.Join(sshPath, "authorized_keys.tmp") + log.Trace("ssh.DeletePublicKey(authorized_keys): %s", p) + + if err = rewriteAuthorizedKeys(key, p, tmpP); err != nil { + return err + } else if err = os.Remove(p); err != nil { + return err + } + return os.Rename(tmpP, p) +} + +// ListPublicKey returns a list of public keys that user has. +func ListPublicKey(userId int64) ([]PublicKey, error) { + keys := make([]PublicKey, 0) + err := orm.Find(&keys, &PublicKey{OwnerId: userId}) + return keys, err +} + +// SaveAuthorizedKeyFile writes SSH key content to SSH key file. +func SaveAuthorizedKeyFile(key *PublicKey) error { + sshOpLocker.Lock() + defer sshOpLocker.Unlock() + + p := filepath.Join(sshPath, "authorized_keys") + f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(GenAuthorizedKey(key.Id, key.Content)) + return err +} diff --git a/models/repo.go b/models/repo.go new file mode 100644 index 0000000000..573e0f4e6f --- /dev/null +++ b/models/repo.go @@ -0,0 +1,647 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "time" + "unicode/utf8" + + "github.com/Unknwon/cae/zip" + "github.com/Unknwon/com" + + "github.com/gogits/git" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" +) + +var ( + ErrRepoAlreadyExist = errors.New("Repository already exist") + ErrRepoNotExist = errors.New("Repository does not exist") + ErrRepoFileNotExist = errors.New("Target Repo file does not exist") + ErrRepoNameIllegal = errors.New("Repository name contains illegal characters") + ErrRepoFileNotLoaded = fmt.Errorf("repo file not loaded") +) + +var ( + LanguageIgns, Licenses []string +) + +func LoadRepoConfig() { + LanguageIgns = strings.Split(base.Cfg.MustValue("repository", "LANG_IGNS"), "|") + Licenses = strings.Split(base.Cfg.MustValue("repository", "LICENSES"), "|") +} + +func NewRepoContext() { + zip.Verbose = false + + // Check if server has basic git setting. + stdout, _, err := com.ExecCmd("git", "config", "--get", "user.name") + if err != nil { + fmt.Printf("repo.init(fail to get git user.name): %v", err) + os.Exit(2) + } else if len(stdout) == 0 { + if _, _, err = com.ExecCmd("git", "config", "--global", "user.email", "gogitservice@gmail.com"); err != nil { + fmt.Printf("repo.init(fail to set git user.email): %v", err) + os.Exit(2) + } else if _, _, err = com.ExecCmd("git", "config", "--global", "user.name", "Gogs"); err != nil { + fmt.Printf("repo.init(fail to set git user.name): %v", err) + os.Exit(2) + } + } +} + +// Repository represents a git repository. +type Repository struct { + Id int64 + OwnerId int64 `xorm:"unique(s)"` + ForkId int64 + LowerName string `xorm:"unique(s) index not null"` + Name string `xorm:"index not null"` + Description string + Website string + NumWatches int + NumStars int + NumForks int + NumIssues int + NumReleases int `xorm:"NOT NULL"` + NumClosedIssues int + NumOpenIssues int `xorm:"-"` + IsPrivate bool + IsBare bool + IsGoget bool + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` +} + +// IsRepositoryExist returns true if the repository with given name under user has already existed. +func IsRepositoryExist(user *User, repoName string) (bool, error) { + repo := Repository{OwnerId: user.Id} + has, err := orm.Where("lower_name = ?", strings.ToLower(repoName)).Get(&repo) + if err != nil { + return has, err + } else if !has { + return false, nil + } + + return com.IsDir(RepoPath(user.Name, repoName)), nil +} + +var ( + illegalEquals = []string{"raw", "install", "api", "avatar", "user", "help", "stars", "issues", "pulls", "commits", "repo", "template", "admin"} + illegalSuffixs = []string{".git"} +) + +// IsLegalName returns false if name contains illegal characters. +func IsLegalName(repoName string) bool { + repoName = strings.ToLower(repoName) + for _, char := range illegalEquals { + if repoName == char { + return false + } + } + for _, char := range illegalSuffixs { + if strings.HasSuffix(repoName, char) { + return false + } + } + return true +} + +// CreateRepository creates a repository for given user or orgnaziation. +func CreateRepository(user *User, repoName, desc, repoLang, license string, private bool, initReadme bool) (*Repository, error) { + if !IsLegalName(repoName) { + return nil, ErrRepoNameIllegal + } + + isExist, err := IsRepositoryExist(user, repoName) + if err != nil { + return nil, err + } else if isExist { + return nil, ErrRepoAlreadyExist + } + + repo := &Repository{ + OwnerId: user.Id, + Name: repoName, + LowerName: strings.ToLower(repoName), + Description: desc, + IsPrivate: private, + IsBare: repoLang == "" && license == "" && !initReadme, + } + repoPath := RepoPath(user.Name, repoName) + + sess := orm.NewSession() + defer sess.Close() + sess.Begin() + + if _, err = sess.Insert(repo); err != nil { + if err2 := os.RemoveAll(repoPath); err2 != nil { + log.Error("repo.CreateRepository(repo): %v", err) + return nil, errors.New(fmt.Sprintf( + "delete repo directory %s/%s failed(1): %v", user.Name, repoName, err2)) + } + sess.Rollback() + return nil, err + } + + access := Access{ + UserName: user.LowerName, + RepoName: strings.ToLower(path.Join(user.Name, repo.Name)), + Mode: AU_WRITABLE, + } + if _, err = sess.Insert(&access); err != nil { + sess.Rollback() + if err2 := os.RemoveAll(repoPath); err2 != nil { + log.Error("repo.CreateRepository(access): %v", err) + return nil, errors.New(fmt.Sprintf( + "delete repo directory %s/%s failed(2): %v", user.Name, repoName, err2)) + } + return nil, err + } + + rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, user.Id); err != nil { + sess.Rollback() + if err2 := os.RemoveAll(repoPath); err2 != nil { + log.Error("repo.CreateRepository(repo count): %v", err) + return nil, errors.New(fmt.Sprintf( + "delete repo directory %s/%s failed(3): %v", user.Name, repoName, err2)) + } + return nil, err + } + + if err = sess.Commit(); err != nil { + sess.Rollback() + if err2 := os.RemoveAll(repoPath); err2 != nil { + log.Error("repo.CreateRepository(commit): %v", err) + return nil, errors.New(fmt.Sprintf( + "delete repo directory %s/%s failed(3): %v", user.Name, repoName, err2)) + } + return nil, err + } + + c := exec.Command("git", "update-server-info") + c.Dir = repoPath + if err = c.Run(); err != nil { + log.Error("repo.CreateRepository(exec update-server-info): %v", err) + } + + if err = NewRepoAction(user, repo); err != nil { + log.Error("repo.CreateRepository(NewRepoAction): %v", err) + } + + if err = WatchRepo(user.Id, repo.Id, true); err != nil { + log.Error("repo.CreateRepository(WatchRepo): %v", err) + } + + if err = initRepository(repoPath, user, repo, initReadme, repoLang, license); err != nil { + return nil, err + } + + return repo, nil +} + +// extractGitBareZip extracts git-bare.zip to repository path. +func extractGitBareZip(repoPath string) error { + z, err := zip.Open("conf/content/git-bare.zip") + if err != nil { + fmt.Println("shi?") + return err + } + defer z.Close() + + return z.ExtractTo(repoPath) +} + +// initRepoCommit temporarily changes with work directory. +func initRepoCommit(tmpPath string, sig *git.Signature) (err error) { + var stderr string + if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "add", "--all"); err != nil { + return err + } + if len(stderr) > 0 { + log.Trace("stderr(1): %s", stderr) + } + + if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), + "-m", "Init commit"); err != nil { + return err + } + if len(stderr) > 0 { + log.Trace("stderr(2): %s", stderr) + } + + if _, stderr, err = com.ExecCmdDir(tmpPath, "git", "push", "origin", "master"); err != nil { + return err + } + if len(stderr) > 0 { + log.Trace("stderr(3): %s", stderr) + } + return nil +} + +func createHookUpdate(hookPath, content string) error { + pu, err := os.OpenFile(hookPath, os.O_CREATE|os.O_WRONLY, 0777) + if err != nil { + return err + } + defer pu.Close() + + _, err = pu.WriteString(content) + return err +} + +// SetRepoEnvs sets environment variables for command update. +func SetRepoEnvs(userId int64, userName, repoName string) { + os.Setenv("userId", base.ToStr(userId)) + os.Setenv("userName", userName) + os.Setenv("repoName", repoName) +} + +// InitRepository initializes README and .gitignore if needed. +func initRepository(f string, user *User, repo *Repository, initReadme bool, repoLang, license string) error { + repoPath := RepoPath(user.Name, repo.Name) + + // Create bare new repository. + if err := extractGitBareZip(repoPath); err != nil { + return err + } + + // hook/post-update + if err := createHookUpdate(filepath.Join(repoPath, "hooks", "update"), + fmt.Sprintf("#!/usr/bin/env bash\n%s update $1 $2 $3\n", + strings.Replace(appPath, "\\", "/", -1))); err != nil { + return err + } + + // Initialize repository according to user's choice. + fileName := map[string]string{} + if initReadme { + fileName["readme"] = "README.md" + } + if repoLang != "" { + fileName["gitign"] = ".gitignore" + } + if license != "" { + fileName["license"] = "LICENSE" + } + + // Clone to temprory path and do the init commit. + tmpDir := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().Nanosecond())) + os.MkdirAll(tmpDir, os.ModePerm) + + if _, _, err := com.ExecCmd("git", "clone", repoPath, tmpDir); err != nil { + return err + } + + // README + if initReadme { + defaultReadme := repo.Name + "\n" + strings.Repeat("=", + utf8.RuneCountInString(repo.Name)) + "\n\n" + repo.Description + if err := ioutil.WriteFile(filepath.Join(tmpDir, fileName["readme"]), + []byte(defaultReadme), 0644); err != nil { + return err + } + } + + // .gitignore + if repoLang != "" { + filePath := "conf/gitignore/" + repoLang + if com.IsFile(filePath) { + if _, err := com.Copy(filePath, + filepath.Join(tmpDir, fileName["gitign"])); err != nil { + return err + } + } + } + + // LICENSE + if license != "" { + filePath := "conf/license/" + license + if com.IsFile(filePath) { + if _, err := com.Copy(filePath, + filepath.Join(tmpDir, fileName["license"])); err != nil { + return err + } + } + } + + if len(fileName) == 0 { + return nil + } + + SetRepoEnvs(user.Id, user.Name, repo.Name) + + // Apply changes and commit. + return initRepoCommit(tmpDir, user.NewGitSig()) +} + +// UserRepo reporesents a repository with user name. +type UserRepo struct { + *Repository + UserName string +} + +// GetRepos returns given number of repository objects with offset. +func GetRepos(num, offset int) ([]UserRepo, error) { + repos := make([]Repository, 0, num) + if err := orm.Limit(num, offset).Asc("id").Find(&repos); err != nil { + return nil, err + } + + urepos := make([]UserRepo, len(repos)) + for i := range repos { + urepos[i].Repository = &repos[i] + u := new(User) + has, err := orm.Id(urepos[i].Repository.OwnerId).Get(u) + if err != nil { + return nil, err + } else if !has { + return nil, ErrUserNotExist + } + urepos[i].UserName = u.Name + } + + return urepos, nil +} + +func RepoPath(userName, repoName string) string { + return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git") +} + +// TransferOwnership transfers all corresponding setting from old user to new one. +func TransferOwnership(user *User, newOwner string, repo *Repository) (err error) { + newUser, err := GetUserByName(newOwner) + if err != nil { + return err + } + + // Update accesses. + accesses := make([]Access, 0, 10) + if err = orm.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repo.LowerName}); err != nil { + return err + } + + sess := orm.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + for i := range accesses { + accesses[i].RepoName = newUser.LowerName + "/" + repo.LowerName + if accesses[i].UserName == user.LowerName { + accesses[i].UserName = newUser.LowerName + } + if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { + return err + } + } + + // Update repository. + repo.OwnerId = newUser.Id + if _, err := sess.Id(repo.Id).Update(repo); err != nil { + sess.Rollback() + return err + } + + // Update user repository number. + rawSql := "UPDATE `user` SET num_repos = num_repos + 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, newUser.Id); err != nil { + sess.Rollback() + return err + } + rawSql = "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, user.Id); err != nil { + sess.Rollback() + return err + } + + // Add watch of new owner to repository. + if !IsWatching(newUser.Id, repo.Id) { + if err = WatchRepo(newUser.Id, repo.Id, true); err != nil { + sess.Rollback() + return err + } + } + + if err = TransferRepoAction(user, newUser, repo); err != nil { + sess.Rollback() + return err + } + + // Change repository directory name. + if err = os.Rename(RepoPath(user.Name, repo.Name), RepoPath(newUser.Name, repo.Name)); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +// ChangeRepositoryName changes all corresponding setting from old repository name to new one. +func ChangeRepositoryName(userName, oldRepoName, newRepoName string) (err error) { + // Update accesses. + accesses := make([]Access, 0, 10) + if err = orm.Find(&accesses, &Access{RepoName: strings.ToLower(userName + "/" + oldRepoName)}); err != nil { + return err + } + + sess := orm.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + for i := range accesses { + accesses[i].RepoName = userName + "/" + newRepoName + if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { + return err + } + } + + // Change repository directory name. + if err = os.Rename(RepoPath(userName, oldRepoName), RepoPath(userName, newRepoName)); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +func UpdateRepository(repo *Repository) error { + repo.LowerName = strings.ToLower(repo.Name) + + if len(repo.Description) > 255 { + repo.Description = repo.Description[:255] + } + if len(repo.Website) > 255 { + repo.Website = repo.Website[:255] + } + _, err := orm.Id(repo.Id).AllCols().Update(repo) + return err +} + +// DeleteRepository deletes a repository for a user or orgnaztion. +func DeleteRepository(userId, repoId int64, userName string) (err error) { + repo := &Repository{Id: repoId, OwnerId: userId} + has, err := orm.Get(repo) + if err != nil { + return err + } else if !has { + return ErrRepoNotExist + } + + sess := orm.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + if _, err = sess.Delete(&Repository{Id: repoId}); err != nil { + sess.Rollback() + return err + } + if _, err := sess.Delete(&Access{RepoName: strings.ToLower(path.Join(userName, repo.Name))}); err != nil { + sess.Rollback() + return err + } + rawSql := "UPDATE `user` SET num_repos = num_repos - 1 WHERE id = ?" + if _, err = sess.Exec(rawSql, userId); err != nil { + sess.Rollback() + return err + } + if _, err = sess.Delete(&Watch{RepoId: repoId}); err != nil { + sess.Rollback() + return err + } + if err = sess.Commit(); err != nil { + sess.Rollback() + return err + } + if err = os.RemoveAll(RepoPath(userName, repo.Name)); err != nil { + // TODO: log and delete manully + log.Error("delete repo %s/%s failed: %v", userName, repo.Name, err) + return err + } + return nil +} + +// GetRepositoryByName returns the repository by given name under user if exists. +func GetRepositoryByName(userId int64, repoName string) (*Repository, error) { + repo := &Repository{ + OwnerId: userId, + LowerName: strings.ToLower(repoName), + } + has, err := orm.Get(repo) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoNotExist + } + return repo, err +} + +// GetRepositoryById returns the repository by given id if exists. +func GetRepositoryById(id int64) (*Repository, error) { + repo := &Repository{} + has, err := orm.Id(id).Get(repo) + if err != nil { + return nil, err + } else if !has { + return nil, ErrRepoNotExist + } + return repo, err +} + +// GetRepositories returns the list of repositories of given user. +func GetRepositories(user *User) ([]Repository, error) { + repos := make([]Repository, 0, 10) + err := orm.Desc("updated").Find(&repos, &Repository{OwnerId: user.Id}) + return repos, err +} + +func GetRepositoryCount(user *User) (int64, error) { + return orm.Count(&Repository{OwnerId: user.Id}) +} + +// Watch is connection request for receiving repository notifycation. +type Watch struct { + Id int64 + RepoId int64 `xorm:"UNIQUE(watch)"` + UserId int64 `xorm:"UNIQUE(watch)"` +} + +// Watch or unwatch repository. +func WatchRepo(userId, repoId int64, watch bool) (err error) { + if watch { + if _, err = orm.Insert(&Watch{RepoId: repoId, UserId: userId}); err != nil { + return err + } + + rawSql := "UPDATE `repository` SET num_watches = num_watches + 1 WHERE id = ?" + _, err = orm.Exec(rawSql, repoId) + } else { + if _, err = orm.Delete(&Watch{0, repoId, userId}); err != nil { + return err + } + rawSql := "UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?" + _, err = orm.Exec(rawSql, repoId) + } + return err +} + +// GetWatches returns all watches of given repository. +func GetWatches(repoId int64) ([]Watch, error) { + watches := make([]Watch, 0, 10) + err := orm.Find(&watches, &Watch{RepoId: repoId}) + return watches, err +} + +// NotifyWatchers creates batch of actions for every watcher. +func NotifyWatchers(act *Action) error { + // Add feeds for user self and all watchers. + watches, err := GetWatches(act.RepoId) + if err != nil { + return errors.New("repo.NotifyWatchers(get watches): " + err.Error()) + } + + // Add feed for actioner. + act.UserId = act.ActUserId + if _, err = orm.InsertOne(act); err != nil { + return errors.New("repo.NotifyWatchers(create action): " + err.Error()) + } + + for i := range watches { + if act.ActUserId == watches[i].UserId { + continue + } + + act.Id = 0 + act.UserId = watches[i].UserId + if _, err = orm.InsertOne(act); err != nil { + return errors.New("repo.NotifyWatchers(create action): " + err.Error()) + } + } + return nil +} + +// IsWatching checks if user has watched given repository. +func IsWatching(userId, repoId int64) bool { + has, _ := orm.Get(&Watch{0, repoId, userId}) + return has +} + +func ForkRepository(reposName string, userId int64) { + +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000000..b2fddd0a1d --- /dev/null +++ b/models/user.go @@ -0,0 +1,479 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/gogits/git" + + "github.com/gogits/gogs/modules/base" + "github.com/gogits/gogs/modules/log" +) + +// User types. +const ( + UT_INDIVIDUAL = iota + 1 + UT_ORGANIZATION +) + +// Login types. +const ( + LT_PLAIN = iota + 1 + LT_LDAP +) + +var ( + ErrUserOwnRepos = errors.New("User still have ownership of repositories") + ErrUserAlreadyExist = errors.New("User already exist") + ErrUserNotExist = errors.New("User does not exist") + ErrEmailAlreadyUsed = errors.New("E-mail already used") + ErrUserNameIllegal = errors.New("User name contains illegal characters") + ErrKeyNotExist = errors.New("Public key does not exist") +) + +// User represents the object of individual and member of organization. +type User struct { + Id int64 + LowerName string `xorm:"unique not null"` + Name string `xorm:"unique not null"` + Email string `xorm:"unique not null"` + Passwd string `xorm:"not null"` + LoginType int + Type int + NumFollowers int + NumFollowings int + NumStars int + NumRepos int + Avatar string `xorm:"varchar(2048) not null"` + AvatarEmail string `xorm:"not null"` + Location string + Website string + IsActive bool + IsAdmin bool + Rands string `xorm:"VARCHAR(10)"` + Salt string `xorm:"VARCHAR(10)"` + Created time.Time `xorm:"created"` + Updated time.Time `xorm:"updated"` +} + +// HomeLink returns the user home page link. +func (user *User) HomeLink() string { + return "/user/" + user.Name +} + +// AvatarLink returns the user gravatar link. +func (user *User) AvatarLink() string { + if base.Service.EnableCacheAvatar { + return "/avatar/" + user.Avatar + } + return "http://1.gravatar.com/avatar/" + user.Avatar +} + +// NewGitSig generates and returns the signature of given user. +func (user *User) NewGitSig() *git.Signature { + return &git.Signature{ + Name: user.Name, + Email: user.Email, + When: time.Now(), + } +} + +// EncodePasswd encodes password to safe format. +func (user *User) EncodePasswd() { + newPasswd := base.PBKDF2([]byte(user.Passwd), []byte(user.Salt), 10000, 50, sha256.New) + user.Passwd = fmt.Sprintf("%x", newPasswd) +} + +// Member represents user is member of organization. +type Member struct { + Id int64 + OrgId int64 `xorm:"unique(member) index"` + UserId int64 `xorm:"unique(member)"` +} + +// IsUserExist checks if given user name exist, +// the user name should be noncased unique. +func IsUserExist(name string) (bool, error) { + if len(name) == 0 { + return false, nil + } + return orm.Get(&User{LowerName: strings.ToLower(name)}) +} + +// IsEmailUsed returns true if the e-mail has been used. +func IsEmailUsed(email string) (bool, error) { + if len(email) == 0 { + return false, nil + } + return orm.Get(&User{Email: email}) +} + +// return a user salt token +func GetUserSalt() string { + return base.GetRandomString(10) +} + +// RegisterUser creates record of a new user. +func RegisterUser(user *User) (*User, error) { + if !IsLegalName(user.Name) { + return nil, ErrUserNameIllegal + } + + isExist, err := IsUserExist(user.Name) + if err != nil { + return nil, err + } else if isExist { + return nil, ErrUserAlreadyExist + } + + isExist, err = IsEmailUsed(user.Email) + if err != nil { + return nil, err + } else if isExist { + return nil, ErrEmailAlreadyUsed + } + + user.LowerName = strings.ToLower(user.Name) + user.Avatar = base.EncodeMd5(user.Email) + user.AvatarEmail = user.Email + user.Rands = GetUserSalt() + user.Salt = GetUserSalt() + user.EncodePasswd() + if _, err = orm.Insert(user); err != nil { + return nil, err + } else if err = os.MkdirAll(UserPath(user.Name), os.ModePerm); err != nil { + if _, err := orm.Id(user.Id).Delete(&User{}); err != nil { + return nil, errors.New(fmt.Sprintf( + "both create userpath %s and delete table record faild: %v", user.Name, err)) + } + return nil, err + } + + if user.Id == 1 { + user.IsAdmin = true + user.IsActive = true + _, err = orm.Id(user.Id).UseBool().Update(user) + } + return user, err +} + +// GetUsers returns given number of user objects with offset. +func GetUsers(num, offset int) ([]User, error) { + users := make([]User, 0, num) + err := orm.Limit(num, offset).Asc("id").Find(&users) + return users, err +} + +// get user by erify code +func getVerifyUser(code string) (user *User) { + if len(code) <= base.TimeLimitCodeLength { + return nil + } + + // use tail hex username query user + hexStr := code[base.TimeLimitCodeLength:] + if b, err := hex.DecodeString(hexStr); err == nil { + if user, err = GetUserByName(string(b)); user != nil { + return user + } + log.Error("user.getVerifyUser: %v", err) + } + + return nil +} + +// verify active code when active account +func VerifyUserActiveCode(code string) (user *User) { + minutes := base.Service.ActiveCodeLives + + if user = getVerifyUser(code); user != nil { + // time limit code + prefix := code[:base.TimeLimitCodeLength] + data := base.ToStr(user.Id) + user.Email + user.LowerName + user.Passwd + user.Rands + + if base.VerifyTimeLimitCode(data, minutes, prefix) { + return user + } + } + return nil +} + +// ChangeUserName changes all corresponding setting from old user name to new one. +func ChangeUserName(user *User, newUserName string) (err error) { + newUserName = strings.ToLower(newUserName) + + // Update accesses of user. + accesses := make([]Access, 0, 10) + if err = orm.Find(&accesses, &Access{UserName: user.LowerName}); err != nil { + return err + } + + sess := orm.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return err + } + + for i := range accesses { + accesses[i].UserName = newUserName + if strings.HasPrefix(accesses[i].RepoName, user.LowerName+"/") { + accesses[i].RepoName = strings.Replace(accesses[i].RepoName, user.LowerName, newUserName, 1) + if err = UpdateAccessWithSession(sess, &accesses[i]); err != nil { + return err + } + } + } + + repos, err := GetRepositories(user) + if err != nil { + return err + } + for i := range repos { + accesses = make([]Access, 0, 10) + // Update accesses of user repository. + if err = orm.Find(&accesses, &Access{RepoName: user.LowerName + "/" + repos[i].LowerName}); err != nil { + return err + } + + for j := range accesses { + accesses[j].RepoName = newUserName + "/" + repos[i].LowerName + if err = UpdateAccessWithSession(sess, &accesses[j]); err != nil { + return err + } + } + } + + // Change user directory name. + if err = os.Rename(UserPath(user.LowerName), UserPath(newUserName)); err != nil { + sess.Rollback() + return err + } + + return sess.Commit() +} + +// UpdateUser updates user's information. +func UpdateUser(user *User) (err error) { + user.LowerName = strings.ToLower(user.Name) + + if len(user.Location) > 255 { + user.Location = user.Location[:255] + } + if len(user.Website) > 255 { + user.Website = user.Website[:255] + } + + _, err = orm.Id(user.Id).AllCols().Update(user) + return err +} + +// DeleteUser completely deletes everything of the user. +func DeleteUser(user *User) error { + // Check ownership of repository. + count, err := GetRepositoryCount(user) + if err != nil { + return errors.New("modesl.GetRepositories: " + err.Error()) + } else if count > 0 { + return ErrUserOwnRepos + } + + // TODO: check issues, other repos' commits + + // Delete all followers. + if _, err = orm.Delete(&Follow{FollowId: user.Id}); err != nil { + return err + } + + // Delete all feeds. + if _, err = orm.Delete(&Action{UserId: user.Id}); err != nil { + return err + } + + // Delete all watches. + if _, err = orm.Delete(&Watch{UserId: user.Id}); err != nil { + return err + } + + // Delete all accesses. + if _, err = orm.Delete(&Access{UserName: user.LowerName}); err != nil { + return err + } + + // Delete all SSH keys. + keys := make([]PublicKey, 0, 10) + if err = orm.Find(&keys, &PublicKey{OwnerId: user.Id}); err != nil { + return err + } + for _, key := range keys { + if err = DeletePublicKey(&key); err != nil { + return err + } + } + + // Delete user directory. + if err = os.RemoveAll(UserPath(user.Name)); err != nil { + return err + } + + _, err = orm.Delete(user) + return err +} + +// UserPath returns the path absolute path of user repositories. +func UserPath(userName string) string { + return filepath.Join(base.RepoRootPath, strings.ToLower(userName)) +} + +func GetUserByKeyId(keyId int64) (*User, error) { + user := new(User) + rawSql := "SELECT a.* FROM `user` AS a, public_key AS b WHERE a.id = b.owner_id AND b.id=?" + has, err := orm.Sql(rawSql, keyId).Get(user) + if err != nil { + return nil, err + } else if !has { + err = errors.New("not exist key owner") + return nil, err + } + return user, nil +} + +// GetUserById returns the user object by given id if exists. +func GetUserById(id int64) (*User, error) { + user := new(User) + has, err := orm.Id(id).Get(user) + if err != nil { + return nil, err + } + if !has { + return nil, ErrUserNotExist + } + return user, nil +} + +// GetUserByName returns the user object by given name if exists. +func GetUserByName(name string) (*User, error) { + if len(name) == 0 { + return nil, ErrUserNotExist + } + user := &User{LowerName: strings.ToLower(name)} + has, err := orm.Get(user) + if err != nil { + return nil, err + } else if !has { + return nil, ErrUserNotExist + } + return user, nil +} + +// GetUserEmailsByNames returns a slice of e-mails corresponds to names. +func GetUserEmailsByNames(names []string) []string { + mails := make([]string, 0, len(names)) + for _, name := range names { + u, err := GetUserByName(name) + if err != nil { + continue + } + mails = append(mails, u.Email) + } + return mails +} + +// GetUserByEmail returns the user object by given e-mail if exists. +func GetUserByEmail(email string) (*User, error) { + if len(email) == 0 { + return nil, ErrUserNotExist + } + user := &User{Email: strings.ToLower(email)} + has, err := orm.Get(user) + if err != nil { + return nil, err + } else if !has { + return nil, ErrUserNotExist + } + return user, nil +} + +// LoginUserPlain validates user by raw user name and password. +func LoginUserPlain(name, passwd string) (*User, error) { + user := User{LowerName: strings.ToLower(name)} + has, err := orm.Get(&user) + if err != nil { + return nil, err + } else if !has { + return nil, ErrUserNotExist + } + + newUser := &User{Passwd: passwd, Salt: user.Salt} + newUser.EncodePasswd() + if user.Passwd != newUser.Passwd { + return nil, ErrUserNotExist + } + return &user, nil +} + +// Follow is connection request for receiving user notifycation. +type Follow struct { + Id int64 + UserId int64 `xorm:"unique(follow)"` + FollowId int64 `xorm:"unique(follow)"` +} + +// FollowUser marks someone be another's follower. +func FollowUser(userId int64, followId int64) (err error) { + session := orm.NewSession() + defer session.Close() + session.Begin() + + if _, err = session.Insert(&Follow{UserId: userId, FollowId: followId}); err != nil { + session.Rollback() + return err + } + + rawSql := "UPDATE `user` SET num_followers = num_followers + 1 WHERE id = ?" + if _, err = session.Exec(rawSql, followId); err != nil { + session.Rollback() + return err + } + + rawSql = "UPDATE `user` SET num_followings = num_followings + 1 WHERE id = ?" + if _, err = session.Exec(rawSql, userId); err != nil { + session.Rollback() + return err + } + return session.Commit() +} + +// UnFollowUser unmarks someone be another's follower. +func UnFollowUser(userId int64, unFollowId int64) (err error) { + session := orm.NewSession() + defer session.Close() + session.Begin() + + if _, err = session.Delete(&Follow{UserId: userId, FollowId: unFollowId}); err != nil { + session.Rollback() + return err + } + + rawSql := "UPDATE `user` SET num_followers = num_followers - 1 WHERE id = ?" + if _, err = session.Exec(rawSql, unFollowId); err != nil { + session.Rollback() + return err + } + + rawSql = "UPDATE `user` SET num_followings = num_followings - 1 WHERE id = ?" + if _, err = session.Exec(rawSql, userId); err != nil { + session.Rollback() + return err + } + return session.Commit() +} |