Статьи

Простой Groovy-трекер с использованием файловой системы

 Хаос не отслеживать ошибки и запросы функций при разработке программного обеспечения. Наличие простого средства отслеживания проблем сделало бы управление проектом гораздо более успешным. Теперь мне нравятся простые вещи, и я думаю, что для небольшого проекта, наличие этого трекера прямо внутри системы управления версиями (особенно с DSVC, например, Mercurial / Git и т. Д.), Не только выполнимо, но и очень удобно. Вам не нужно сходить с ума от всех причудливых функций, но достаточно просто для отслеживания проблем. Я хотел бы предложить этот макет для вас.

Допустим, у вас есть проект, который выглядит так

project
 +- src/main/java/Hello.java
 +- issues/issue-001.md
 +- pom.xml

Все, что мне нужно, это простой каталог, issuesчтобы начать работу. Теперь у меня есть место для отслеживания моей проблемы! Первая проблема issue-000.mdдолжна быть о том, о чем ваш проект. Например:

/id=issue-001
/createdon=2012-12-16 18:07:08
/type=bug
/status=new
/resolution=open
/from=zemian
/to=
/found=v1.0.0
/fixed=
/subject=A simple Java Hello program

# Updated on 2012-12-16 18:07:08

We want to create a Maven based Hello world program. It should print "Hello World."

Я выбираю .mdрасширение файла для намерения писать комментарии в формате Markdown. Поскольку это текстовый файл, вы делаете то, что хотите. Чтобы быть более структурированным, я добавил несколько метаданных заголовков для отслеживания проблем. Давайте определим некоторые здесь. Я бы предложил использовать их и форматирование:

 /id=issue-
 /createdon=
 /type=feature|bug|question
 /status=new|reviewing|working|testing|fixed
 /resolution=open|closed|rejected|hold
 /from=
 /to=
 /found=
 /fixed=

Это должно охватывать большинство проблем с ошибками и разработкой функций. Это не круто писать программы без истории изменений, включая эти проблемы. Итак, давайте использовать источник контроля. Я настоятельно рекомендую вам использовать Mercurial hg. Вы можете создать и инициализировать новый репозиторий следующим образом.

bash> cd project
bash> hg init
bash> hg add
bash> hg commit -m "My hello world project"

Теперь ваш проект создан, и у нас есть место для отслеживания ваших проблем. Теперь это простой текстовый файл, поэтому используйте ваш любимый текстовый редактор и редактируйте его. Однако создание новой проблемы с этими тегами заголовка скучно. Будет неплохо иметь скрипт, который немного справится с этим. У меня есть скрипт Groovy issue.groovy(см. В конце этой статьи), который позволяет запускать отчеты и создавать новые проблемы. Вы можете добавить этот скрипт в свой project/issuesкаталог и мгновенно создавать новые отчеты о проблемах и запросах! Вот пример вывода на моем ПК:

bash> cd project
bash> groovy scripts/issue.groovy

Searching for issues with /resolution=open
Issue: /id=issue-001 /status=new /subject=A simple Java Hello program
1 issues found.

bash> groovy scripts/issue.groovy --new /type=feature /subject='Add a unit test.'

project\issues\issue-002.md created.
/id=issue-002
/createdon=2012-12-16 19:10:00
/type=feature
/status=new
/resolution=open
/from=zemian
/to=
/found=v1.0.0
/fixed=
/subject=Add a unit test.

bash> groovy scripts/issue.groovy

Searching for issues with /resolution=open
Issue: /id=issue-000 /status=new /subject=A simple Java Hello program
Issue: /id=issue-002 /status=new /subject=Add a unit test.
2 issues found.

bash> groovy scripts/issue.groovy --details /id=002

Searching for issues with /id=002
Issue: /id=issue-002
  /createdon=2012-12-16 19:10:00 /found=v1.0.0, /from=zemian, /resolution=open, /status=new /type=feature
  /subject=Add a unit test.
1 issues found.

bash> groovy scripts/issue.groovy --update /id=001 /status=fixed /resolution=closed 'I fixed this thang.'
Updating issue /id=issue-001
Updating /status=fixed
Updating /resolution=closed

Update issue-001 completed.

Сценарий дает вам быстрый и последовательный способ создания / обновления / поиска проблем. Но это просто текстовые файлы! Вы также можете запустить ваш любимый текстовый редактор и изменить любую вещь, которую вы хотите. Сохраните и даже зафиксируйте его в вашем исходном хранилище. Все не будет потеряно.

Я надеюсь, что этот скрипт отслеживания ошибок поможет быстро запустить ваш следующий проект. Дай мне знать, что ты думаешь!

Наслаждайтесь!

Земян Вот мой issue.groovyсценарий.

#!/usr/bin/env groovy
//
// A groovy script to manage issue files and its metadata/headers.
// Created by Zemian Deng <[email protected]> 12/2012
// 
// Usage:
//  bash> groovy [java_opts] issue.groovy [option] [/header_name=value...] [arguments]
//
// Examples:
//   # Report all issues that match headers (we support RegEx!)
//   bash> groovy issue /resolution=open
//   bash> groovy issue /subject='Improve UI|service'
//   bash> groovy issue --details /status=fixed
//
//   # Create a new bug issue file.
//   bash> groovy issue --new /type=bug /to=zemian /found=v1.0.1 /subject='I found some problem.' 'More details here.'
//
//   # Update an issue
//   bash> groovy issue --update /id=issue-001 /status=fixed /resolution=closed 'I fixed this issue with Z algorithm.'
//

class issue {
        def ISSUES_HEADERS = ['/id', '/createdon', '/type', '/status', '/resolution', '/from', '/to', '/found', '/fixed', '/subject']
        def ISSUES_HEADERS_VALS = [
                '/type' : ['feature', 'bug', 'question'] as Set, 
                '/status' : ['new', 'reviewing', 'working', 'testing', 'fixed'] as Set, 
                '/resolution' : ['open', 'closed', 'rejected', 'hold'] as Set
                ]
        def issuesDir = new File(System.getProperty("issuesDir", getDefaultIssuesDir()))
        def issuePrefix = System.getProperty("issuePrefix", 'issue')
        def arguments = [] // script arguments after parsing
        def options = [:]  // script options after parsing
        def headers = [:]  // user input issue headers

        static void main(String[] args) {
                new issue().run(args)
        }

        // Method declarations
        def run(String[] args) {
                // Parse and save options, arguments and headers vars
                def headersSet = ISSUES_HEADERS.toSet()
                args.each { arg ->
                        def append = true
                        if (arg =~ /^--{0,1}\w+/) {
                                options[arg] = true
                                append = false
                        } else if (arg =~ /^\/\w+=.*$/) {
                                def words = arg.split('=')
                                if (words.length == 2) {
                                        def name = words[0]
                                        def value = words[1]
                                        
                                        if (!headersSet.contains(name)) 
                                                throw new Exception("ERROR: Unkown header name $name.")
                                        if (ISSUES_HEADERS_VALS[name] != null && !(ISSUES_HEADERS_VALS[name].contains(value)))
                                                throw new Exception("ERROR: Unkown header $name=$value. Allowed: ${ISSUES_HEADERS_VALS[name].join(', ')}")

                                        headers.put(name, words[1])
                                        append = false
                                }
                        }

                        if (append) {
                                arguments << arg
                        }
                }

                // support short option flag
                if (options['-d'])
                        options['--details'] = true

                // Run script depending on options passed
                if (options['--help'] || options['-h']) {
                        printHelp()
                } else if (options['--new'] || options['-n']) {
                        createIssue()
                } else if (options['--update'] || options['-u']) {
                        updateIssue()
                } else {
                        reportIssues()
                }
        }

        def printHelp() {
                new File(getClass().protectionDomain.codeSource.location.path).withReader{ reader ->
                        def done = false
                        def line = null
                        while (!done && (line = reader.readLine()) != null) {
                                line = line.trim()
                                if (line.startsWith("#") || line.startsWith("//"))
                                        println(line)
                                else
                                        done = true
                        }
                }
        }

        def getDefaultIssuesDir() {     
                return new File(getClass().protectionDomain.codeSource.location.path).parentFile.path 
        }

        def getIssueIds() {
                def issueIds = []
                def files = issuesDir.listFiles()
                if (files == null)
                        return issueIds
                files.each{ f ->
                        def m = f.name =~ /^(\w+-\d+)\.md/
                        if (m)
                                issueIds << m[0][1]
                }
                return issueIds
        }

        def getIssueFile(String issueid) {
                return new File(issuesDir, "${issueid}.md")
        }

        def reportIssues() {
                if (headers.size() == 0)
                        headers['/resolution'] = 'open'
                def headersLine = headers.sort{ a,b -> a.key <=> b.key }.collect{ k,v -> "$k=$v" }.join(', ')   
                println "Searching for issues with $headersLine"
                def count = 0
                getIssueIds().each { issueid ->
                        def file = getIssueFile(issueid)
                        def issueHeaders = [:]
                        file.withReader{ reader ->
                                def done = false
                                def line = null
                                while (!done && (line = reader.readLine()) != null) {
                                        if (line =~ /^\/\w+=.*$/) {
                                                def words = line.split('=')
                                                if (words.length >= 2) {
                                                        issueHeaders.put(words[0], words[1..-1].join('='))
                                                }
                                        } else if (issueHeaders.size() > 0) {
                                                done = true
                                        }
                                }
                        }
                        def match = headers.findAll{ k,v -> (issueHeaders[k] =~ /${v}/) ? true : false }
                        if (match.size() == headers.size()) {
                                def line = "Issue: /id=${issueHeaders['/id']}"
                                if (options['--details']) {
                                        def col = 4
                                        def issueHeadersKeys = issueHeaders.keySet().sort() - ['/id', '/subject']
                                        issueHeadersKeys.collate(col).each { set ->
                                                line += "\n  " + set.collect{ k -> "$k=${issueHeaders[k]}" }.join(" ")
                                        }
                                        line += "\n  /subject=${issueHeaders['/subject']}"
                                } else {
                                        line +=
                                                " /status=${issueHeaders['/status']}" +
                                                " /subject=${issueHeaders['/subject']}"
                                }
                                println line
                                count += 1
                        }
                }
                println "$count issues found."
        }

        def createIssue() {
                def ids = getIssueIds().collect{ issueid -> issueid.split('-')[1].toInteger() }
                def nextid = ids.size() > 0 ? ids.max() + 1 : 1
                def issueid = String.format("${issuePrefix}-%03d", nextid)
                def file = getIssueFile(issueid)
                def createdon = new Date().format('yyyy-MM-dd HH:mm:ss')
                def newHeaders = [
                        '/id' :  issueid,
                        '/createdon' :  createdon,
                        '/type' : 'bug',
                        '/status' : 'new',
                        '/resolution' : 'open',
                        '/from' : System.properties['user.name'],
                        '/to' :  '',
                        '/found' : 'v1.0.0',
                        '/fixed' : '',
                        '/subject' : 'A bug report'
                ]
                // Override newHeaders from user inputs
                headers.each { k,v -> newHeaders.put(k, v) }

                //Output to file
                file.withWriter{ writer ->
                        ISSUES_HEADERS.each{ k -> writer.println("$k=${newHeaders[k]}") }
                        writer.println()
                        writer.println("# Updated on ${createdon}")
                        writer.println()
                        arguments.each { 
                                writer.println(it) 
                                writer.println()
                        }
                        writer.println()
                }

                // Output issue headers to STDOUT
                println "$file created."
                ISSUES_HEADERS.each{ k -> println("$k=${newHeaders[k]}") }
        }

        def updateIssue() {
                def userHeaders = new HashMap(headers)
                userHeaders.remove('/createdon') // we should not update this field
                def issueid = userHeaders.remove('/id') // We will not re-update /id
                if (issueid == null)
                        throw new Exception("Failed to update issue: missing /id value.")
                if (!issueid.startsWith(issuePrefix))
                        issueid = "${issuePrefix}-${issueid}"
                println("Updating issue /id=${issueid}")

                def file = getIssueFile(issueid)
                def newFile = new File(file.parentFile, "${file.name}.update.tmp")
                def hasUpdate = false
                def issueHeaders = [:]

                if (!file.exists())
                        throw new Exception("Failed to update issue: file not found for /id=${issueid}")

                // Read and update issue headers
                file.withReader{ reader ->
                        // Read all issue headers first
                        def done = false
                        def line = null
                        while (!done && (line = reader.readLine()) != null) {
                                if (line =~ /^\/\w+=.*$/) {
                                        def words = line.split('=')
                                        if (words.length >= 2) {
                                                issueHeaders.put(words[0], words[1..-1].join('='))
                                        }
                                } else if (issueHeaders.size() > 0) {
                                        done = true
                                }
                        }

                        // Find issue headers differences
                        userHeaders.each{ k,v -> 
                                if (issueHeaders[k] != v) {
                                        println("Updating $k=$v")
                                        issueHeaders[k] = v
                                        if (!hasUpdate)
                                                hasUpdate = true
                                }
                        }

                        // Update issue file
                        if (hasUpdate) {
                                newFile.withWriter{ writer ->
                                        ISSUES_HEADERS.each{ k -> writer.println("${k}=${issueHeaders[k] ?: ''}") }
                                        writer.println()

                                        // Write/copy the rest of the file.
                                        done = false
                                        while (!done && (line = reader.readLine()) != null) {
                                                writer.println(line)
                                        }
                                        writer.println()
                                }
                        }
                } // reader

                if (hasUpdate) {
                        // Rename the new file back to orig
                        file.delete()
                        newFile.renameTo(file)
                }

                // Append any arguments as user comments
                if (arguments.size() > 0) {
                        file.withWriterAppend{ writer ->
                                writer.println()
                                writer.println("# Updated on ${new Date().format('yyyy-MM-dd HH:mm:ss')}")
                                writer.println()
                                arguments.each{ text -> 
                                        writer.println(text)
                                        writer.println()
                                }
                                println()
                        }
                }

                println("Update $issueid completed.")
        }
}