On Github miguelcoba / grails-intro
$ grails -version Grails version: 2.1.0
$ grails create-app tutorial $ cd tutorial
$ grails create-controller hello
grails-app/controllers/tutorial/HelloController.groovy
package tutorial class HelloController { def world = { render "Hello World!" } }
$ grails run-app
$ grails integrate-with --intellij
$ grails integrate-with --textmate
grails-app - top level app dir conf - Configuration files controllers - web controllers domain - application domain models i18n - i18n resource files services - services layer taglib - custom tag libraries views - Groovy Server Pages scripts - scripts for grails src - Supporting sources test - unit, integration and functional tests
$ grails run-app
$ grails test-app
$ grails war
$ grails generate-all tutorial.Hello
$ grails create-controller $ grails create-domain-class $ grails create-unit-test $ grails create-tag-lib
grails/conf/Config.groovy
Set values
my.app.value = "some value"
Read values (in controllers/services/taglibs)
grailsApplication.config.my.app.value
import org.codehaus.groovy.grails.commons.* CodeHolder.config.my.app.hello
log4j = { error 'com.example.package1', 'com.example.package2' warn 'com.example.package3' }
Class loading
org.codejaus.groovy.grails.commons
Web request processing
org.codejaus.groovy.grails.web
URL mapping
org.codejaus.groovy.grails.web.mapping
Plugin activity
org.codejaus.groovy.grails.plugins
Spring activity
org.springframework
Hibernate activity
org.hibernate
grails.gorm.failOnError = true throws Exception on validation failure on save() method grails.gorm.autoFlush = true to force flushing of Hibernate session on save(), delete(), merge() methods
Config files
Application code
grails [environment] [command name] grails run-app // runs app in default mode (default is dev) grails prod run-app // runs app in production mode grails test war // creates war for the test environment
import grails.util.Environment switch(Environment.current) { case Environment.DEVELOPMENT: someConfigForDev() break case Environment.PRODUCTION: someConfigForProd() break }
def init = { ServletContext ctx -> environments { production { ctx.setAttribute("env", "prod") } development { someConfigForDev() } } someConfigForAllEnvironments() }
import grails.util.Environment Environment.executeForCurrentEnvironment { production { someConfig() } development { someOtherConfig() } }
dependencies { // mysql runtime 'mysql:mysql-connector-java:5.1.5' // sqlserver runtime 'net.sourceforge.jtds:jtds:1.2.4' }
dataSource { pooled = true dbCreate = "update" url = "jdbc:mysql://localhost/yourDB" driverClassName = "com.mysql.jdbc.Driver" dialect = org.hibernate.dialect.MySQL5InnoDBDialect username = "yourUser" password = "yourPassword" properties { maxActive = 50 maxIdle = 25 minIdle = 5 initialSize = 5 minEvictableIdleTimeMillis = 60000 timeBetweenEvictionRunsMillis = 60000 maxWait = 10000 validationQuery = "/* ping */" } }
grails.config.locations = [ "classpath:${appName}-config.properties", "classpath:${appName}-config.groovy", "file:${userHome}/.grails/${appName}-config.properties", "file:${userHome}/.grails/${appName}-config.groovy" ] read: grailsApplication
// set $ grails set-version 0.99 application.properties // read in controllers def version = grailsApplication.metadata['app.version'] def grailsVer = grailsApplication.metadata['app.grails.version'] def version = grails.util.GrailsUtil.grailsVersion
Textile variation
src/doc/guide/1. first chapter.gdoc src/doc/guide/2. this will be the second chapter.gdoc
$ grails doc # generate documentation
// group:name:version runtime "com.mysql:mysql-connector-java:5.1.5" runtime(group: 'com.mysql', name: 'mysql-connector-java', version: '5.1.5') // plugin dependencies plugins { test ':spock:0.5-groovy' runtime ':jquery:1.4.2.7' }
USER_HOME/.grails/scripts PROJECT_HOME/scripts PROJECT_HOME/plugins/*/scripts GRAILS_HOME/scripts
Script
$ grails run-app
Searches
USER_HOME/.grails/scripts/RunApp.groovy PROJECT_HOME/scripts/RunApp.groovy PLUGINS_HOME/*/scripts/RunApp.groovy GLOBAL_PLUGINS_HOME/*/scripts/RunApp.groovy GRAILS_HOME/scripts/RunApp.groovy
$ grails integrate-with --ant build.xml
$ grails create-pom com.mycompany pom.xml
mysql -u root -p create database tutorial; create user tutorial@localhost identified by 'tutorial'; grant all on tutorial.* to tutorial@localhost;
grails-app/conf/DataSource.groovy: environments { development { dataSource { dbCreate = "create-drop" // one of 'create', 'create-drop','update' //loggingSql = true url = "jdbc:mysql://localhost/tutorial" driverClassName = "com.mysql.jdbc.Driver" dialect = org.hibernate.dialect.MySQL5InnoDBDialect username = "tutorial" password = "tutorial" } } }
grails-app/conf/BuildConfig.groovy: mavenCentral()
grails-app/conf/BuildConfig.groovy: dependencies { runtime 'mysql:mysql-connector-java:5.1.5' }
$ grails create-app tutorial // default package: tutorial $ grails create-domain-class Person
package tutorial class Person { static constraints = { } }
package tutorial class Person { String name Integer age Date lastVisit static constraints = { } }
// grails console import tutorial.* // save def p = new Person(name: 'Miguel', age: 31, lastVisit: new Date()) p.save() // read p = Person.get(1) println p.name // update p = Person.get(1) p.name = "Bob" p.save() // delete p = Person.get(1) p.delete() // list def l = Person.list() l.each { println "name:${it.name}, age:${it.age}, lastVisit:${it.lastVisit}" }
$ grails create-domain-class Face $ grails create-domain-class Nose $ grails create-domain-class Book $ grails create-domain-class Author
class Face { Nose nose // property } class Nose { }
mysql> describe face; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | nose_id | bigint(20) | NO | MUL | NULL | | +---------+------------+------+-----+---------+----------------+ mysql> describe nose; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | +---------+------------+------+-----+---------+----------------+
class Face { Nose nose static constraints = { nose unique: true } } class Nose { }
mysql> describe face; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | nose_id | bigint(20) | NO | UNI | NULL | | +---------+------------+------+-----+---------+----------------+ mysql> describe nose; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | +---------+------------+------+-----+---------+----------------+
class Face { Nose nose } class Nose { static belongsTo = [ face:Face ] }
mysql> describe face; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | nose_id | bigint(20) | NO | MUL | NULL | | +---------+------------+------+-----+---------+----------------+ mysql> describe nose; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | +---------+------------+------+-----+---------+----------------+
// Nose is saved automatically new Face(nose: new Nose()).save()
// Won't work. Face is transient new Nose(face: new Face()).save()
def f = new Face(1) f.delete() // Face and Nose are deleted
class Face { static hasOne = [ nose:Nose ] } class Nose { Face face }
mysql> describe face; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | +---------+------------+------+-----+---------+----------------+ mysql> describe nose; +---------+------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | face_id | bigint(20) | NO | UNI | NULL | | +---------+------------+------+-----+---------+----------------+
class Author { static hasMany = [ books:Book ] String name } class Book { String title }
mysql> describe author; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | +---------+--------------+------+-----+---------+----------------+ mysql> describe book; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | title | varchar(255) | NO | | NULL | | +---------+--------------+------+-----+---------+----------------+ mysql> describe author_book; +-----------------+------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-----------------+------------+------+-----+---------+-------+ | author_books_id | bigint(20) | YES | MUL | NULL | | | book_id | bigint(20) | YES | MUL | NULL | | +-----------------+------------+------+-----+---------+-------+
import tutorial.* def a = new Author(name: 'Tolkien') a.addToBooks(title: 'The Hobbit') a.addToBooks(title: 'The Lord of the Rings') a.save() a.books.each { println it.title }
mysql> select * from author; +----+---------+---------+ | id | version | name | +----+---------+---------+ | 1 | 0 | Tolkien | +----+---------+---------+ mysql> select * from book; +----+---------+-----------------------+ | id | version | title | +----+---------+-----------------------+ | 1 | 0 | The Lord of the Rings | | 2 | 0 | The Hobbit | +----+---------+-----------------------+ mysql> select * from author_book; +-----------------+---------+ | author_books_id | book_id | +-----------------+---------+ | 1 | 1 | | 1 | 2 | +-----------------+---------+
// save/update are cascaded // deletes are not cascaded import tutorial.* def a = Author.get(1) a.delete() Author.list().size() // 0 Book.list().size() // 2
mysql> select * from author; Empty set (0.00 sec) mysql> select * from book; +----+---------+-----------------------+ | id | version | title | +----+---------+-----------------------+ | 1 | 0 | The Lord of the Rings | | 2 | 0 | The Hobbit | +----+---------+-----------------------+ 2 rows in set (0.00 sec) mysql> select * from author_book; Empty set (0.00 sec)
class Author { static hasMany = [ books:Book ] String name } class Book { static belongsTo = [ author:Author ] String title }
mysql> describe author; +---------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | name | varchar(255) | NO | | NULL | | +---------+--------------+------+-----+---------+----------------+ mysql> describe book; +-----------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-----------+--------------+------+-----+---------+----------------+ | id | bigint(20) | NO | PRI | NULL | auto_increment | | version | bigint(20) | NO | | NULL | | | author_id | bigint(20) | NO | MUL | NULL | | | title | varchar(255) | NO | | NULL | | +-----------+--------------+------+-----+---------+----------------+
import tutorial.* def a = new Author(name: 'Tolkien') def b = new Book(title: 'The Hobbit') def b2 = new Book(title: 'The Lord of the Rings') a.addToBooks(b) a.addToBooks(b2) a.save() println(a.books.size()) // 2 a.books.each { println it.title } // The Hobbit // The Lord of the Rings println b.author.name // Tolkien println b2.author.name // Tolkien
mysql> select * from author; +----+---------+---------+ | id | version | name | +----+---------+---------+ | 1 | 0 | Tolkien | +----+---------+---------+ mysql> select * from book; +----+---------+-----------+-----------------------+ | id | version | author_id | title | +----+---------+-----------+-----------------------+ | 1 | 0 | 1 | The Hobbit | | 2 | 0 | 1 | The Lord of the Rings | +----+---------+-----------+-----------------------+
class Book { static belongsTo = Author static hasMany = [ authors:Author ] String title } class Author { static hasMany = [ books:Book ] String name }
import tutorial.* new Author(name: 'Tolkien') .addToBooks(new Book(title: 'The Hobbit')) .addToBooks(new Book(title: 'The Lord of the Rings')) .save() println Author.list().size() // 1 println Book.list().size() // 2
mysql> select * from author; +----+---------+---------+ | id | version | name | +----+---------+---------+ | 1 | 0 | Tolkien | +----+---------+---------+ mysql> select * from book; +----+---------+-----------------------+ | id | version | title | +----+---------+-----------------------+ | 1 | 0 | The Hobbit | | 2 | 0 | The Lord of the Rings | +----+---------+-----------------------+ mysql> select * from author_books; +-----------+---------+ | author_id | book_id | +-----------+---------+ | 1 | 1 | | 1 | 2 | +-----------+---------+
import tutorial.* new Book(title: 'The C programming language') .addToAuthors(name: 'Kernighan') .addToAuthors(name: 'Ritchie') .save() println Author.list().size() // 1 println Book.list().size() // 3
mysql> select * from author; +----+---------+---------+ | id | version | name | +----+---------+---------+ | 1 | 0 | Tolkien | +----+---------+---------+ mysql> select * from book; +----+---------+----------------------------+ | id | version | title | +----+---------+----------------------------+ | 1 | 0 | The Hobbit | | 2 | 0 | The Lord of the Rings | | 3 | 0 | The C programming language | +----+---------+----------------------------+ mysql> select * from author_books; +-----------+---------+ | author_id | book_id | +-----------+---------+ | 1 | 1 | | 1 | 2 | +-----------+---------+
def p = Person.get(1) p.name = "Bob" p.save() // no SQL update guaranteed in that point // Hibernate batches SQL statements def p = Person.get(1) p.name = "Bob" p.save(flush: true) // Forces a synchronization with DB // Handling exceptions on saving def p = Person.get(1) try { p.save(flush:true) } catch(Exception e) { // deal with exception }
def p = Person.get(1) p.delete() // same as save/update def p = Person.get(1) p.delete(flush:true) // Forces synchronization with DB // Handling exceptions def p = Person.get(1) try { p.delete(flush:true) } catch(org.springframework.dao.DataIntegrityViolationException e) { // deal with exception }
http://spring.io/blog/2010/06/23/gorm-gotchas-part-1
// by default lazy class Location { String city } class Author { String name Location location } Author.list().each { author -> println author.location.city } // If there are N = 4 authors // 1 query to fetch all authors // 1 query per author to get the location (because we're printing the city) // Total = 4 + 1 = N + 1 querys
class Author { String name Location location static mapping = { location fetch: 'join' } } Author.list(fetch: [location: 'join']).each { a -> println a.location.city }
// Dynamic finders Author.findAllByNameLike("John%", [ sort: 'name', order: 'asc', fetch: [location: 'join'] ]).each { a -> // ... } // Criteria queries def authors = Author.withCriteria { like("name", "John%") join "location" }
// list def books = Book.list() def books = Book.list(offset:10, max:20) def books = Book.list(sort:"title", order: "desc") // retrieval def b = Book.get(23) def list = Book.getAll(1,3,24)
class Book { String title Date releaseDate Author author } class Author { String name } def b b = Book.findByTitle("The Hobbit") b = Book.findByTitleLike("%Hobb%") b = Book.findByReleaseDateBetween(firstDate, secondDate) b = Book.findByReleaseDateGreaterThan(someDate) b = Book.findByTitleLikeOrReleaseDateLessThan("%obbi%", someDate) b = Book.findByReleaseDateIsNull() b = Book.findByReleaseDateIsNotNull() b = Book.findAllByTitleLike("The %", [max:3, offset:10, sort: "title", order: "desc"])
def c = Book.createCriteria() def results = c { eq("releaseDate", someDate) or { like("title", "%programming%") like("title", "%Ring%") } maxResults(100) order("title", "desc") }
def list = Book.findAll( "from Book as b where b.title like 'Lord of the%'") def list = Book.findAll( "from Book as b where b.author = ?", [author]) def list = Book.findAll( "from Book as b where b.author = :author", [author:someAuthor])
$ grails create-controller book // default package: tutorial
package tutorial class BookController { def index = {} } // mapped to /book URI
// properties that are assigned a block of code // each property maps to an URI // public by default class BookController { def list = { // some statements } } // maps to /book/list
static defaultAction = "list"if only one action exists, the default URI maps to it if an index action exists, it handle requests when no action specified explicit declaration
class BookController { def find = { def findBy = params["findBy"] def userAgent = request.getHeader("User-Agent") def loggedUser = session["logged_user"] // session.logged_user } def delete = { def b = Book.get( params.id ) if(!b) { flash.message = "User not found for id ${params.id}" redirect(action:list) } } } }
def show = { [ book: Book.get(params.id) ] }
class BookController { List books List authors def list = { books = Book.list() authors = Author.list() } }
class BookController { def show = { [ book:Book.get(params.id) ] } }
def show = { def map = [ book: Book.get(1) ] render(view: "display", model: map) }
def show = { def map = [ book: Book.get(1) ] render(view: "/shared/display", model: map) }
class BookController { def greet = { render "hello!" } }
class BookController { def greet = { render "hello!" } def redirect = { redirect(action: greet) } }
redirect(action:list)
redirect(controller: 'author', action: 'list')
redirect(uri: "/help.html")
redirect(url: 'http://yahoo.com')
// implicit constructor def save = { def b = new Book(params) b.save() } // explicit binding def save = { def b = Book.get(params.id) b.properties = params // sets every parameter // as a property in the object b.someParam = params.foo // only some parameters are set b.otherParam = params.bar b.save() }
def listXML = { def results = Book.list() render(contentType: 'text/xml') { books { for(b in results) { book(title: b.title) } } } }
<books> <book title="title one"></book> <book title="title two"></book> </books>
def listJSON = { def results = Book.list() render(contentType: 'text/json') { books = array { for(b in results) { book(title: b.title) } } } }
"books":[ {"title": "title one"}, {"title": "title two"} ]
import grails.converters.* def list = { render Book.list() as XML } def list2 = { render Book.list().encodeAsXML() // using codecs }
render Book.list() as JSON render Book.list().encodeAsJSON()
def total = params.int('total') def checked = params.boolean('checked')
// returns a model with key called book. def show = { [ book: Book.get(1) ] }
<%-- the key named book from the model is referenced by name in the gsp --%> <%= book.title %>
<% %> blocks to embed groovy code (discouraged) <html> <body> <% out << "Hello world!" %> <%= "this is equivalent" %> </body> </html>
<% now = new Date() %> ... <p>Time: <%= now %></p> <!-- predefined: - application - applicationContext - flash - grailsApplication - out - params - request - response - session - webRequest -->
<html> <body> Hello ${params.name} Time is: ${new Date()} 2 + 2 is: ${ 2 + 2 } but also: ${ /* any valid groovy statement */ } </body> </html>
<g:example param="a string param" otherParam="${new Date()}" aMap="[aString:'a string', aDate: new Date()]"> Hello world </g:example>
<g:set var="myVar" value="${new Date()}"/> <g:set var="myText"> some text with expressions ${myVar} </g:set> <p>${myText}</p>
<g:if test="${1 > 3}"> some html </g:if> <g:else> something else </g:else> <g:each in="${tutorial.Book.list()}" var="b"> title: ${b.title} </g:each> <g:set var="n" value="${0}"/> <g:while test="${n<5}"> ${n++} </g:while>
<g:link action="show" id="2">Item 2</g:link> <g:link controller="user" action="list">Users</g:link>
<g:form name="myForm" url="[controller:'book', action:'submit']"> Text: <g:textField name="text"/> <g:submitButton name="button" value="Submit form"/> </g:form>
Static Resource: ${createLinkTo(dir:"images", file:"logo.jpg")} <img src="${createLinkTo(dir:'images', file:'logo.jpg')}" />
def imageLocation = createLinkTo(dir:"images", file:"logo.jpg") def imageLocation = g.createLinkTo(dir:"images", file:"logo.jpg")
<div class="book" id="${book.id}"> Title: ${book.title} </div>
<g:render template="info" var="book" collection="${tutorial.Book.list()}"/>
def useTemplate = { def b = Book.get(1) render(template: "info", model: [ book:b ]) }
Add "showSource=true" to the url. Only in development mode
Add "debugTemplates" to the url. Only in development mode
class UtilsTagLib { def copyright = { attrs, body -> out << "© Copyright 2014" } }
<div><g:copyright/></div>
<div>© Copyright 2014</div>
import java.text.* def dateFormat = { attrs, body -> def fmt = new SimpleDateFormat(attrs.format) out << fmt.format(attrs.date) }
Date: <g:dateFormat format="dd-MM-yyyy" date="${new Date()}"/>
Date: 23-04-2014
def formatBook = { attrs, body -> out << render(template: "info", model: [ book:attrs.book ]) }
<div><g:formatBook book="${tutorial.Book.get(1)}"/></div>
<div> <div class="book" id="1"> Title: Some title </div> </div>
<head> <g:javascript library="prototype"/> <g:javascript library="scriptaculous"/> </head>
<g:remoteLink action="delete" id="1"> Delete Book </g:remoteLink>
asynchronous request to the delete action of the current controller with an id parameter with value of 1
class AjaxController { def index = {} def delete = { def b = Book.get(params.id) b.delete() render "Book ${b.title} was deleted" } }
Success: <div id="success"></div> Failure: <div id="error"></div> <g:remoteLink action="success" update="[success:'success', failure:'error']"> Success ajax request </g:remoteLink><br/> <g:remoteLink action="failure" update="[success:'success', failure:'error']"> Failure ajax request </g:remoteLink>
def success = { render status:200, text:"request OK" } def failure = { render status:503, text:"request failed" }
<g:formRemote url="[controller:'ajax', action:'ajaxAdd']" name="ajaxForm" update="[success:'addSuccess', failure:'addError']"> Book title: <input type="text" name="title"/> <input type="submit" value="Add Book!" /> </g:formRemote > <div id="addSuccess"></div> <div id="addError"></div>
def ajaxAdd = { def b = new Book(params) b.save() render "Book '${b.title}' created" }
<g:remoteLink action="ajaxContent" update="book"> Update Content </g:remoteLink> <div id="book"><!--existing book mark-up --></div>
def ajaxContent = { def b = Book.get(2) render "Book: <strong>${b.title}</strong> found at ${new Date()}!" }
<g:javascript> function updateBook(e) { // evaluate the JSON var book = eval("("+e.responseText+")") $("book_title").innerHTML = book.title } </g:javascript> <g:remoteLink action="ajaxData" update="foo" onSuccess="updateBook(e)"> Update Book with JSON </g:remoteLink> <div id="book"> <div id="book_title">The Hobbit</div> </div>
import grails.converters.* def ajaxData = { def b = new Book(title: 'new book title').save() render b as JSON }
$ svnadmin create /home/miguel/repo $ svn list file:///home/miguel/repo
$ svn mkdir file:///home/miguel/repo/GrailsProject $ svn mkdir file:///home/miguel/repo/GrailsProject/branches $ svn mkdir file:///home/miguel/repo/GrailsProject/tags $ svn mkdir file:///home/miguel/repo/GrailsProject/trunk
$ grails create-app GrailsProject
$ cd GrailsProject $ svn co file:///home/miguel/repo/GrailsProject/trunk . $ svn add .classpath .settings .project * $ svn propset svn:ignore "WEB-INF" web-app/ $ svn propset svn:ignore "target" . $ svn rm --force web-app/WEB-INF $ svn commit -m "First commit of GrailsProject"
$ cd .. $ svn co file:///home/miguel/repo/GrailsProject/trunk OtherUserGrailsProject $ cd OtherUserGrailsProject $ grails upgrade
$ grails create-app GrailsProject $ cd GrailsProject $ git init .gitignore: web-app/WEB-INF/ target/ $ git add . $ git commit -m "First commit on GrailsProject"
$ cd .. $ git clone GrailsProject OtherUserGrailsProject $ cd OtherUserGrailsProject $ grails upgrade
Miguel Cobá
Github: miguelcoba