| // |
| // Copyright (c) 2011 Kevin Sawicki <kevinsawicki@gmail.com> |
| // |
| // Permission is hereby granted, free of charge, to any person obtaining a copy |
| // of this software and associated documentation files (the "Software"), to |
| // deal in the Software without restriction, including without limitation the |
| // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or |
| // sell copies of the Software, and to permit persons to whom the Software is |
| // furnished to do so, subject to the following conditions: |
| // |
| // The above copyright notice and this permission notice shall be included in |
| // all copies or substantial portions of the Software. |
| // |
| // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| // IN THE SOFTWARE. |
| // |
| // Copyright (C) 2017 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.googlesource.gerrit.plugins.analytics.test |
| |
| import java.io.{File, PrintWriter} |
| import java.nio.file.Files |
| import java.text.MessageFormat |
| import java.util.Date |
| |
| import com.googlesource.gerrit.plugins.analytics.common.DateConversions.isoStringToLongDate |
| import com.googlesource.gerrit.plugins.analytics.common.ManagedResource.use |
| import org.eclipse.jgit.api.MergeCommand.FastForwardMode |
| import org.eclipse.jgit.api.{Git, MergeResult} |
| import org.eclipse.jgit.api.errors.GitAPIException |
| import org.eclipse.jgit.lib.{Constants, PersonIdent, Ref} |
| import org.eclipse.jgit.merge.MergeStrategy |
| import org.eclipse.jgit.notes.Note |
| import org.eclipse.jgit.revwalk.RevCommit |
| import org.gitective.core.CommitUtils |
| import org.scalatest.{BeforeAndAfterEach, Suite} |
| import com.googlesource.gerrit.plugins.analytics.common.DateConversions._ |
| |
| |
| /** |
| * Base test case with utilities for common Git operations performed during |
| * testing |
| */ |
| trait GitTestCase extends BeforeAndAfterEach { |
| self: Suite => |
| /** |
| * Test repository .git directory |
| */ |
| protected var testRepo: File = null |
| |
| /** |
| * Author used for commits |
| */ |
| protected val author = new PersonIdent("Test Author", "author@test.com") |
| |
| def newPersonIdent(name: String = "Test Person", email: String = "person@test.com", ts: Date = new Date()) = |
| new PersonIdent(new PersonIdent(name, email), ts) |
| |
| /** |
| * Committer used for commits |
| */ |
| protected val committer = new PersonIdent("Test Committer", "committer@test.com") |
| |
| /** |
| * Set up method that initializes git repository |
| * |
| * |
| */ |
| |
| override def beforeEach = { |
| testRepo = initRepo |
| } |
| |
| /** |
| * Initialize a new repo in a new directory |
| * |
| * @return created .git folder |
| * @throws GitAPIException |
| */ |
| protected def initRepo: File = { |
| val dir = Files.createTempDirectory(threadSpecificDirectoryPrefix).toFile |
| Git.init.setDirectory(dir).setBare(false).call |
| val repo = new File(dir, Constants.DOT_GIT) |
| assert(repo.exists) |
| repo.deleteOnExit() |
| repo |
| } |
| |
| private def threadSpecificDirectoryPrefix = |
| "git-test-case-" + Thread.currentThread().getName.replaceAll("~[0-9a-zA-Z]", "_") + System.nanoTime |
| |
| /** |
| * Create branch with name and checkout |
| * |
| * @param name |
| * @return branch ref |
| * |
| */ |
| protected def branch(name: String): Ref = branch(testRepo, name) |
| |
| /** |
| * Delete branch with name |
| * |
| * @param name |
| * @return branch ref |
| * |
| */ |
| protected def deleteBranch(name: String): String = deleteBranch(testRepo, name) |
| |
| /** |
| * Create branch with name and checkout |
| * |
| * @param repo |
| * @param name |
| * @return branch ref |
| * |
| */ |
| protected def branch(repo: File, name: String): Ref = { |
| use(Git.open(repo)) { git => |
| git.branchCreate.setName(name).call |
| checkout(repo, name) |
| } |
| } |
| |
| /** |
| * Delete branch with name |
| * |
| * @param repo |
| * @param name |
| * @return branch ref |
| * |
| */ |
| protected def deleteBranch(repo: File, name: String): String = { |
| use(Git.open(repo)) { git => |
| git.branchDelete().setBranchNames(name).call.get(0) |
| } |
| } |
| |
| /** |
| * Checkout branch |
| * |
| * @param name |
| * @return branch ref |
| * |
| */ |
| protected def checkout(name: String): Ref = checkout(testRepo, name) |
| |
| /** |
| * Checkout branch |
| * |
| * @param repo |
| * @param name |
| * @return branch ref |
| * |
| */ |
| @throws[Exception] |
| protected def checkout(repo: File, name: String): Ref = { |
| use(Git.open(repo)) { git => |
| git.checkout.setName(name).call |
| } |
| } ensuring(_ != null, "Unable to checkout result") |
| |
| /** |
| * Create tag with name |
| * |
| * @param name |
| * @return tag ref |
| * |
| */ |
| protected def tag(name: String): Ref = tag(testRepo, name) |
| |
| /** |
| * Create tag with name |
| * |
| * @param repo |
| * @param name |
| * @return tag ref |
| * |
| */ |
| protected def tag(repo: File, name: String): Ref = { |
| use(Git.open(repo)) { git => |
| git.tag.setName(name).setMessage(name).call |
| git.getRepository.getTags.get(name) |
| } |
| } ensuring(_ != null, s"Unable to tag file $name") |
| |
| /** |
| * Add file to test repository |
| * |
| * @param path |
| * @param content |
| * @return commit |
| * |
| */ |
| protected def add(path: String, content: String, author: PersonIdent = author, committer: PersonIdent = committer): RevCommit = add(testRepo, path, content, author, committer) |
| |
| /** |
| * Add file to test repository |
| * |
| * @param repo |
| * @param path |
| * @param content |
| * @return commit |
| * |
| */ |
| protected def add(repo: File, path: String, content: String, author: PersonIdent, committer: PersonIdent): RevCommit = { |
| val message = MessageFormat.format("Committing {0} at {1}", path, new Date) |
| add(repo, path, content, message, author, committer) |
| } |
| |
| |
| /** |
| * Add file to test repository |
| * |
| * @param repo |
| * @param path |
| * @param content |
| * @param message |
| * @return commit |
| * |
| */ |
| protected def add(repo: File, path: String, content: String, message: String, author: PersonIdent, committer: PersonIdent): RevCommit = { |
| val file = new File(repo.getParentFile, path) |
| if (!file.getParentFile.exists) assert(file.getParentFile.mkdirs) |
| if (!file.exists) assert(file.createNewFile) |
| |
| val writer = new PrintWriter(file) |
| try |
| writer.print(Option(content).fold("")(identity)) |
| finally writer.close() |
| |
| use(Git.open(repo)) { git => |
| git.add.addFilepattern(path).call |
| git.commit.setOnly(path).setMessage(message).setAuthor(author).setCommitter(committer).call |
| } |
| } ensuring (_ != null, s"Unable to commit addition of path $path") |
| |
| /** |
| * Move file in test repository |
| * |
| * @param from |
| * @param to |
| * @return commit |
| * |
| */ |
| protected def mv(from: String, to: String): RevCommit = mv(testRepo, from, to, MessageFormat.format("Moving {0} to {1} at {2}", from, to, new Date)) |
| |
| /** |
| * Move file in test repository |
| * |
| * @param from |
| * @param to |
| * @param message |
| * @return commit |
| * |
| */ |
| protected def mv(from: String, to: String, message: String): RevCommit = mv(testRepo, from, to, message) |
| |
| /** |
| * Move file in test repository |
| * |
| * @param repo |
| * @param from |
| * @param to |
| * @param message |
| * @return commit |
| * |
| */ |
| protected def mv(repo: File, from: String, to: String, message: String): RevCommit = { |
| val file = new File(repo.getParentFile, from) |
| file.renameTo(new File(repo.getParentFile, to)) |
| |
| use(Git.open(testRepo)) { git => |
| git.rm.addFilepattern(from) |
| git.add.addFilepattern(to).call |
| git.commit.setAll(true).setMessage(message).setAuthor(author).setCommitter(committer).call |
| } |
| } ensuring (_ != null, "Unable to commit MV operation") |
| |
| /** |
| * Add files to test repository |
| * |
| * @param contents iterable of file names and associated content |
| * @return commit |
| * |
| */ |
| protected def add(contents: Iterable[(String, String)]): RevCommit = add(testRepo, contents, "Committing multiple files") |
| |
| /** |
| * Add files to test repository |
| * |
| * @param repo |
| * @param contents iterable of file names and associated content |
| * @param message |
| * @return commit |
| * |
| */ |
| protected def add(repo: File, contents: Iterable[(String, String)], message: String): RevCommit = { |
| use(Git.open(testRepo)) { git => |
| var i = 0 |
| contents.foreach { case (path, content) => |
| val file = new File(repo.getParentFile, path) |
| if (!file.getParentFile.exists) require(file.getParentFile.mkdirs, s"Cannot create parent dir '${file.getParent}'") |
| if (!file.exists) require(file.createNewFile, s"Cannot create file '$file'") |
| val writer = new PrintWriter(file) |
| try |
| writer.print(content) |
| finally writer.close() |
| git.add.addFilepattern(path).call |
| } |
| |
| git.commit.setMessage(message).setAuthor(author).setCommitter(committer).call |
| } |
| } ensuring (_ != null, "Unable to commit content addition") |
| |
| /** |
| * Merge given branch into current branch |
| * |
| * @param branch |
| * @return result |
| * |
| */ |
| protected def mergeBranch(branch: String, withCommit: Boolean): MergeResult = { |
| use(Git.open(testRepo)) { git => |
| git.merge.setStrategy(MergeStrategy.RESOLVE).include(CommitUtils.getRef(git.getRepository, branch)).setCommit(withCommit).setFastForward(FastForwardMode.NO_FF).setMessage(s"merging branch $branch").call |
| } |
| } |
| |
| /** |
| * Merge ref into current branch |
| * |
| * @param ref |
| * @return result |
| * |
| */ |
| protected def merge(ref: String): MergeResult = { |
| use(Git.open(testRepo)) { git => |
| git.merge.setStrategy(MergeStrategy.RESOLVE).include(CommitUtils.getCommit(git.getRepository, ref)).call |
| } |
| } |
| |
| /** |
| * Add note to latest commit with given content |
| * |
| * @param content |
| * @return note |
| * |
| */ |
| protected def note(content: String): Note = note(content, "commits") |
| |
| /** |
| * Add note to latest commit with given content |
| * |
| * @param content |
| * @param ref |
| * @return note |
| * |
| */ |
| protected def note(content: String, ref: String): Note = { |
| use(Git.open(testRepo)) { git => |
| git.notesAdd.setMessage(content).setNotesRef(Constants.R_NOTES + ref).setObjectId(CommitUtils.getHead(git.getRepository)).call |
| } |
| } ensuring (_ != null, "Unable to add note") |
| |
| /** |
| * Delete and commit file at path |
| * |
| * @param path |
| * @return commit |
| * |
| */ |
| protected def delete(path: String): RevCommit = { |
| val message = MessageFormat.format("Committing {0} at {1}", path, new Date) |
| use(Git.open(testRepo)) { git => |
| git.rm.addFilepattern(path).call |
| git.commit.setOnly(path).setMessage(message).setAuthor(author).setCommitter(committer).call |
| } |
| } ensuring (_ != null, "Unable to commit delete operation") |
| |
| /** |
| * commit specified content into a file, as committer |
| * |
| * @param committer - the author of this commit |
| * @param fileName - the name of the file |
| * @param content - the content of the file |
| * @param when - the date of the commit |
| * @return RevCommit |
| * |
| */ |
| protected def commit(committer: String, fileName: String, content: String, when: Date = new Date(), message: Option[String] = None): RevCommit = { |
| val person = newPersonIdent(committer, committer, when) |
| add(testRepo, fileName, content, author = person, committer = author, message = message.getOrElse("** no message **")) |
| } |
| |
| /** |
| * commit specified content into a file, as committer and merge into current branch |
| * |
| * @param committer - the author of this commit |
| * @param fileName - the name of the file |
| * @param content - the content of the file |
| * @return MergeResult |
| * |
| */ |
| protected def mergeCommit(committer: String, fileName: String, content: String): MergeResult = { |
| val currentBranch = Git.open(testRepo).getRepository.getBranch |
| val tmpBranch = branch(testRepo, "tmp") |
| try { |
| commit(committer, fileName, content) |
| checkout(currentBranch) |
| mergeBranch(tmpBranch.getName, withCommit = true) |
| } finally { |
| deleteBranch(testRepo, tmpBranch.getName) |
| } |
| } |
| } |