You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

update.go 13 kB

[Enhancement] Allow admin to merge pr with protected file changes (#12078) * [Enhancement] Allow admin to merge pr with protected file changes As tilte, show protected message in diff page and merge box. Signed-off-by: a1012112796 <1012112796@qq.com> * remove unused ver * Update options/locale/locale_en-US.ini Co-authored-by: Cirno the Strongest <1447794+CirnoT@users.noreply.github.com> * Add TrN * Apply suggestions from code review * fix lint * Update options/locale/locale_en-US.ini Co-authored-by: zeripath <art27@cantab.net> * Apply suggestions from code review * move pr proteced files check to TestPatch * Call TestPatch when protected branches settings changed * Apply review suggestion @CirnoT * move to service @lunny * slightly restructure routers/private/hook.go Adds a lot of comments and simplifies the logic Signed-off-by: Andrew Thornton <art27@cantab.net> * placate lint Signed-off-by: Andrew Thornton <art27@cantab.net> * skip duplicate protected files check * fix check logic * slight refactor of TestPatch Signed-off-by: Andrew Thornton <art27@cantab.net> * When checking for protected files changes in TestPatch use the temporary repository Signed-off-by: Andrew Thornton <art27@cantab.net> * fix introduced issue with hook Signed-off-by: Andrew Thornton <art27@cantab.net> * Remove the check on PR index being greater than 0 as it unnecessary Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <matti@mdranta.net> Co-authored-by: Cirno the Strongest <1447794+CirnoT@users.noreply.github.com> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
5 years ago
Improve listing performance by using go-git (#6478) * Use go-git for tree reading and commit info lookup. Signed-off-by: Filip Navara <navara@emclient.com> * Use TreeEntry.IsRegular() instead of ObjectType that was removed. Signed-off-by: Filip Navara <navara@emclient.com> * Use the treePath to optimize commit info search. Signed-off-by: Filip Navara <navara@emclient.com> * Extract the latest commit at treePath along with the other commits. Signed-off-by: Filip Navara <navara@emclient.com> * Fix listing commit info for a directory that was created in one commit and never modified after. Signed-off-by: Filip Navara <navara@emclient.com> * Avoid nearly all external 'git' invocations when doing directory listing (.editorconfig code path is still hit). Signed-off-by: Filip Navara <navara@emclient.com> * Use go-git for reading blobs. Signed-off-by: Filip Navara <navara@emclient.com> * Make SHA1 type alias for plumbing.Hash in go-git. Signed-off-by: Filip Navara <navara@emclient.com> * Make Signature type alias for object.Signature in go-git. Signed-off-by: Filip Navara <navara@emclient.com> * Fix GetCommitsInfo for repository with only one commit. Signed-off-by: Filip Navara <navara@emclient.com> * Fix PGP signature verification. Signed-off-by: Filip Navara <navara@emclient.com> * Fix issues with walking commit graph across merges. Signed-off-by: Filip Navara <navara@emclient.com> * Fix typo in condition. Signed-off-by: Filip Navara <navara@emclient.com> * Speed up loading branch list by keeping the repository reference (and thus all the loaded packfile indexes). Signed-off-by: Filip Navara <navara@emclient.com> * Fix lising submodules. Signed-off-by: Filip Navara <navara@emclient.com> * Fix build Signed-off-by: Filip Navara <navara@emclient.com> * Add back commit cache because of name-rev Signed-off-by: Filip Navara <navara@emclient.com> * Fix tests Signed-off-by: Filip Navara <navara@emclient.com> * Fix code style * Fix spelling * Address PR feedback Signed-off-by: Filip Navara <navara@emclient.com> * Update vendor module list Signed-off-by: Filip Navara <navara@emclient.com> * Fix getting trees by commit id Signed-off-by: Filip Navara <navara@emclient.com> * Fix remaining unit test failures * Fix GetTreeBySHA * Avoid running `git name-rev` if not necessary Signed-off-by: Filip Navara <navara@emclient.com> * Move Branch code to git module * Clean up GPG signature verification and fix it for tagged commits * Address PR feedback (import formatting, copyright headers) * Make blob lookup by SHA working * Update tests to use public API * Allow getting content from any type of object through the blob interface * Change test to actually expect the object content that is in the GIT repository * Change one more test to actually expect the object content that is in the GIT repository * Add comments
6 years ago
Add configurable Trust Models (#11712) * Add configurable Trust Models Gitea's default signature verification model differs from GitHub. GitHub uses signatures to verify that the committer is who they say they are - meaning that when GitHub makes a signed commit it must be the committer. The GitHub model prevents re-publishing of commits after revocation of a key and prevents re-signing of other people's commits to create a completely trusted repository signed by one key or a set of trusted keys. The default behaviour of Gitea in contrast is to always display the avatar and information related to a signature. This allows signatures to be decoupled from the committer. That being said, allowing arbitary users to present other peoples commits as theirs is not necessarily desired therefore we have a trust model whereby signatures from collaborators are marked trusted, signatures matching the commit line are marked untrusted and signatures that match a user in the db but not the committer line are marked unmatched. The problem with this model is that this conflicts with Github therefore we need to provide an option to allow users to choose the Github model should they wish to. Signed-off-by: Andrew Thornton <art27@cantab.net> * Adjust locale strings Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @6543 Co-authored-by: 6543 <6543@obermui.de> * Update models/gpg_key.go * Add migration for repository Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
5 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repofiles
  5. import (
  6. "bytes"
  7. "fmt"
  8. "path"
  9. "strings"
  10. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/charset"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/lfs"
  15. "code.gitea.io/gitea/modules/log"
  16. repo_module "code.gitea.io/gitea/modules/repository"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/storage"
  19. "code.gitea.io/gitea/modules/structs"
  20. stdcharset "golang.org/x/net/html/charset"
  21. "golang.org/x/text/transform"
  22. )
  23. // IdentityOptions for a person's identity like an author or committer
  24. type IdentityOptions struct {
  25. Name string
  26. Email string
  27. }
  28. // CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE
  29. type CommitDateOptions struct {
  30. Author time.Time
  31. Committer time.Time
  32. }
  33. // UpdateRepoFileOptions holds the repository file update options
  34. type UpdateRepoFileOptions struct {
  35. LastCommitID string
  36. OldBranch string
  37. NewBranch string
  38. TreePath string
  39. FromTreePath string
  40. Message string
  41. Content string
  42. SHA string
  43. IsNewFile bool
  44. Author *IdentityOptions
  45. Committer *IdentityOptions
  46. Dates *CommitDateOptions
  47. }
  48. func detectEncodingAndBOM(entry *git.TreeEntry, repo *models.Repository) (string, bool) {
  49. reader, err := entry.Blob().DataAsync()
  50. if err != nil {
  51. // return default
  52. return "UTF-8", false
  53. }
  54. defer reader.Close()
  55. buf := make([]byte, 1024)
  56. n, err := reader.Read(buf)
  57. if err != nil {
  58. // return default
  59. return "UTF-8", false
  60. }
  61. buf = buf[:n]
  62. if setting.LFS.StartServer {
  63. meta := lfs.IsPointerFile(&buf)
  64. if meta != nil {
  65. meta, err = repo.GetLFSMetaObjectByOid(meta.Oid)
  66. if err != nil && err != models.ErrLFSObjectNotExist {
  67. // return default
  68. return "UTF-8", false
  69. }
  70. }
  71. if meta != nil {
  72. dataRc, err := lfs.ReadMetaObject(meta)
  73. if err != nil {
  74. // return default
  75. return "UTF-8", false
  76. }
  77. defer dataRc.Close()
  78. buf = make([]byte, 1024)
  79. n, err = dataRc.Read(buf)
  80. if err != nil {
  81. // return default
  82. return "UTF-8", false
  83. }
  84. buf = buf[:n]
  85. }
  86. }
  87. encoding, err := charset.DetectEncoding(buf)
  88. if err != nil {
  89. // just default to utf-8 and no bom
  90. return "UTF-8", false
  91. }
  92. if encoding == "UTF-8" {
  93. return encoding, bytes.Equal(buf[0:3], charset.UTF8BOM)
  94. }
  95. charsetEncoding, _ := stdcharset.Lookup(encoding)
  96. if charsetEncoding == nil {
  97. return "UTF-8", false
  98. }
  99. result, n, err := transform.String(charsetEncoding.NewDecoder(), string(buf))
  100. if err != nil {
  101. // return default
  102. return "UTF-8", false
  103. }
  104. if n > 2 {
  105. return encoding, bytes.Equal([]byte(result)[0:3], charset.UTF8BOM)
  106. }
  107. return encoding, false
  108. }
  109. // CreateOrUpdateRepoFile adds or updates a file in the given repository
  110. func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *UpdateRepoFileOptions) (*structs.FileResponse, error) {
  111. // If no branch name is set, assume default branch
  112. if opts.OldBranch == "" {
  113. opts.OldBranch = repo.DefaultBranch
  114. }
  115. if opts.NewBranch == "" {
  116. opts.NewBranch = opts.OldBranch
  117. }
  118. // oldBranch must exist for this operation
  119. if _, err := repo_module.GetBranch(repo, opts.OldBranch); err != nil {
  120. return nil, err
  121. }
  122. // A NewBranch can be specified for the file to be created/updated in a new branch.
  123. // Check to make sure the branch does not already exist, otherwise we can't proceed.
  124. // If we aren't branching to a new branch, make sure user can commit to the given branch
  125. if opts.NewBranch != opts.OldBranch {
  126. existingBranch, err := repo_module.GetBranch(repo, opts.NewBranch)
  127. if existingBranch != nil {
  128. return nil, models.ErrBranchAlreadyExists{
  129. BranchName: opts.NewBranch,
  130. }
  131. }
  132. if err != nil && !git.IsErrBranchNotExist(err) {
  133. return nil, err
  134. }
  135. } else {
  136. protectedBranch, err := repo.GetBranchProtection(opts.OldBranch)
  137. if err != nil {
  138. return nil, err
  139. }
  140. if protectedBranch != nil {
  141. if !protectedBranch.CanUserPush(doer.ID) {
  142. return nil, models.ErrUserCannotCommit{
  143. UserName: doer.LowerName,
  144. }
  145. }
  146. if protectedBranch.RequireSignedCommits {
  147. _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch)
  148. if err != nil {
  149. if !models.IsErrWontSign(err) {
  150. return nil, err
  151. }
  152. return nil, models.ErrUserCannotCommit{
  153. UserName: doer.LowerName,
  154. }
  155. }
  156. }
  157. patterns := protectedBranch.GetProtectedFilePatterns()
  158. for _, pat := range patterns {
  159. if pat.Match(strings.ToLower(opts.TreePath)) {
  160. return nil, models.ErrFilePathProtected{
  161. Path: opts.TreePath,
  162. }
  163. }
  164. }
  165. }
  166. }
  167. // If FromTreePath is not set, set it to the opts.TreePath
  168. if opts.TreePath != "" && opts.FromTreePath == "" {
  169. opts.FromTreePath = opts.TreePath
  170. }
  171. // Check that the path given in opts.treePath is valid (not a git path)
  172. treePath := CleanUploadFileName(opts.TreePath)
  173. if treePath == "" {
  174. return nil, models.ErrFilenameInvalid{
  175. Path: opts.TreePath,
  176. }
  177. }
  178. // If there is a fromTreePath (we are copying it), also clean it up
  179. fromTreePath := CleanUploadFileName(opts.FromTreePath)
  180. if fromTreePath == "" && opts.FromTreePath != "" {
  181. return nil, models.ErrFilenameInvalid{
  182. Path: opts.FromTreePath,
  183. }
  184. }
  185. message := strings.TrimSpace(opts.Message)
  186. author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
  187. t, err := NewTemporaryUploadRepository(repo)
  188. if err != nil {
  189. log.Error("%v", err)
  190. }
  191. defer t.Close()
  192. if err := t.Clone(opts.OldBranch); err != nil {
  193. return nil, err
  194. }
  195. if err := t.SetDefaultIndex(); err != nil {
  196. return nil, err
  197. }
  198. // Get the commit of the original branch
  199. commit, err := t.GetBranchCommit(opts.OldBranch)
  200. if err != nil {
  201. return nil, err // Couldn't get a commit for the branch
  202. }
  203. // Assigned LastCommitID in opts if it hasn't been set
  204. if opts.LastCommitID == "" {
  205. opts.LastCommitID = commit.ID.String()
  206. } else {
  207. lastCommitID, err := t.gitRepo.ConvertToSHA1(opts.LastCommitID)
  208. if err != nil {
  209. return nil, fmt.Errorf("DeleteRepoFile: Invalid last commit ID: %v", err)
  210. }
  211. opts.LastCommitID = lastCommitID.String()
  212. }
  213. encoding := "UTF-8"
  214. bom := false
  215. executable := false
  216. if !opts.IsNewFile {
  217. fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
  218. if err != nil {
  219. return nil, err
  220. }
  221. if opts.SHA != "" {
  222. // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
  223. if opts.SHA != fromEntry.ID.String() {
  224. return nil, models.ErrSHADoesNotMatch{
  225. Path: treePath,
  226. GivenSHA: opts.SHA,
  227. CurrentSHA: fromEntry.ID.String(),
  228. }
  229. }
  230. } else if opts.LastCommitID != "" {
  231. // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
  232. // an error, but only if we aren't creating a new branch.
  233. if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
  234. if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil {
  235. return nil, err
  236. } else if changed {
  237. return nil, models.ErrCommitIDDoesNotMatch{
  238. GivenCommitID: opts.LastCommitID,
  239. CurrentCommitID: opts.LastCommitID,
  240. }
  241. }
  242. // The file wasn't modified, so we are good to delete it
  243. }
  244. } else {
  245. // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits
  246. // haven't been made. We throw an error if one wasn't provided.
  247. return nil, models.ErrSHAOrCommitIDNotProvided{}
  248. }
  249. encoding, bom = detectEncodingAndBOM(fromEntry, repo)
  250. executable = fromEntry.IsExecutable()
  251. }
  252. // For the path where this file will be created/updated, we need to make
  253. // sure no parts of the path are existing files or links except for the last
  254. // item in the path which is the file name, and that shouldn't exist IF it is
  255. // a new file OR is being moved to a new path.
  256. treePathParts := strings.Split(treePath, "/")
  257. subTreePath := ""
  258. for index, part := range treePathParts {
  259. subTreePath = path.Join(subTreePath, part)
  260. entry, err := commit.GetTreeEntryByPath(subTreePath)
  261. if err != nil {
  262. if git.IsErrNotExist(err) {
  263. // Means there is no item with that name, so we're good
  264. break
  265. }
  266. return nil, err
  267. }
  268. if index < len(treePathParts)-1 {
  269. if !entry.IsDir() {
  270. return nil, models.ErrFilePathInvalid{
  271. Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
  272. Path: subTreePath,
  273. Name: part,
  274. Type: git.EntryModeBlob,
  275. }
  276. }
  277. } else if entry.IsLink() {
  278. return nil, models.ErrFilePathInvalid{
  279. Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
  280. Path: subTreePath,
  281. Name: part,
  282. Type: git.EntryModeSymlink,
  283. }
  284. } else if entry.IsDir() {
  285. return nil, models.ErrFilePathInvalid{
  286. Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath),
  287. Path: subTreePath,
  288. Name: part,
  289. Type: git.EntryModeTree,
  290. }
  291. } else if fromTreePath != treePath || opts.IsNewFile {
  292. // The entry shouldn't exist if we are creating new file or moving to a new path
  293. return nil, models.ErrRepoFileAlreadyExists{
  294. Path: treePath,
  295. }
  296. }
  297. }
  298. // Get the two paths (might be the same if not moving) from the index if they exist
  299. filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath)
  300. if err != nil {
  301. return nil, fmt.Errorf("UpdateRepoFile: %v", err)
  302. }
  303. // If is a new file (not updating) then the given path shouldn't exist
  304. if opts.IsNewFile {
  305. for _, file := range filesInIndex {
  306. if file == opts.TreePath {
  307. return nil, models.ErrRepoFileAlreadyExists{
  308. Path: opts.TreePath,
  309. }
  310. }
  311. }
  312. }
  313. // Remove the old path from the tree
  314. if fromTreePath != treePath && len(filesInIndex) > 0 {
  315. for _, file := range filesInIndex {
  316. if file == fromTreePath {
  317. if err := t.RemoveFilesFromIndex(opts.FromTreePath); err != nil {
  318. return nil, err
  319. }
  320. }
  321. }
  322. }
  323. content := opts.Content
  324. if bom {
  325. content = string(charset.UTF8BOM) + content
  326. }
  327. if encoding != "UTF-8" {
  328. charsetEncoding, _ := stdcharset.Lookup(encoding)
  329. if charsetEncoding != nil {
  330. result, _, err := transform.String(charsetEncoding.NewEncoder(), content)
  331. if err != nil {
  332. // Look if we can't encode back in to the original we should just stick with utf-8
  333. log.Error("Error re-encoding %s (%s) as %s - will stay as UTF-8: %v", opts.TreePath, opts.FromTreePath, encoding, err)
  334. result = content
  335. }
  336. content = result
  337. } else {
  338. log.Error("Unknown encoding: %s", encoding)
  339. }
  340. }
  341. // Reset the opts.Content to our adjusted content to ensure that LFS gets the correct content
  342. opts.Content = content
  343. var lfsMetaObject *models.LFSMetaObject
  344. if setting.LFS.StartServer {
  345. // Check there is no way this can return multiple infos
  346. filename2attribute2info, err := t.CheckAttribute("filter", treePath)
  347. if err != nil {
  348. return nil, err
  349. }
  350. if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" {
  351. // OK so we are supposed to LFS this data!
  352. oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content))
  353. if err != nil {
  354. return nil, err
  355. }
  356. lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID}
  357. content = lfsMetaObject.Pointer()
  358. }
  359. }
  360. // Add the object to the database
  361. objectHash, err := t.HashObject(strings.NewReader(content))
  362. if err != nil {
  363. return nil, err
  364. }
  365. // Add the object to the index
  366. if executable {
  367. if err := t.AddObjectToIndex("100755", objectHash, treePath); err != nil {
  368. return nil, err
  369. }
  370. } else {
  371. if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
  372. return nil, err
  373. }
  374. }
  375. // Now write the tree
  376. treeHash, err := t.WriteTree()
  377. if err != nil {
  378. return nil, err
  379. }
  380. // Now commit the tree
  381. var commitHash string
  382. if opts.Dates != nil {
  383. commitHash, err = t.CommitTreeWithDate(author, committer, treeHash, message, opts.Dates.Author, opts.Dates.Committer)
  384. } else {
  385. commitHash, err = t.CommitTree(author, committer, treeHash, message)
  386. }
  387. if err != nil {
  388. return nil, err
  389. }
  390. if lfsMetaObject != nil {
  391. // We have an LFS object - create it
  392. lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
  393. if err != nil {
  394. return nil, err
  395. }
  396. contentStore := &lfs.ContentStore{ObjectStorage: storage.LFS}
  397. exist, err := contentStore.Exists(lfsMetaObject)
  398. if err != nil {
  399. return nil, err
  400. }
  401. if !exist {
  402. if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil {
  403. if _, err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
  404. return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
  405. }
  406. return nil, err
  407. }
  408. }
  409. }
  410. // Then push this tree to NewBranch
  411. if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
  412. log.Error("%T %v", err, err)
  413. return nil, err
  414. }
  415. commit, err = t.GetCommit(commitHash)
  416. if err != nil {
  417. return nil, err
  418. }
  419. file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath)
  420. if err != nil {
  421. return nil, err
  422. }
  423. return file, nil
  424. }