Хаос не отслеживать ошибки и запросы функций при разработке программного обеспечения. Наличие простого средства отслеживания проблем сделало бы управление проектом гораздо более успешным. Теперь мне нравятся простые вещи, и я думаю, что для небольшого проекта, наличие этого трекера прямо внутри системы управления версиями (особенно с 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 <saltnlight5@gmail.com> 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.")
}
}