我刚写完一个简单的版本控制。使用"add“注册文件,然后"com”将它们保存在一个目录中,并附加一个ID,所有文件的ID相同。使用"rev“,它将复制文件的内容,但不会删除除"commitdir”中的文件之外的任何其他文件。
package main
import (
"encoding/gob"
"errors"
"fmt"
//"github.com/davecgh/go-spew/spew"
//"io"
"io/ioutil"
"log"
"os"
"strconv"
)
type DB struct {
filename string
dirname string
CommitList map[string]bool
ID int
file *os.File
}
func LoadDB(filename, dirname string) (*DB, error) {
er := ger("Loading DB")
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return nil, er("Opening file", err)
}
dec := gob.NewDecoder(file)
db := &DB{}
stats, err := file.Stat()
if err != nil {
return nil, er("Getting file stats", err)
}
if stats.Size() > 0 {
err = dec.Decode(db)
if err != nil {
return nil, er("Decoding non empty db", err)
}
} else {
db = &DB{}
db.CommitList = make(map[string]bool)
}
db.filename = filename
db.dirname = dirname
db.file = file
return db, nil
}
func (db *DB) Write() error {
er := ger("Writing DB")
_, err := db.file.Seek(0, 0)
if err != nil {
return er("File seek", err)
}
enc := gob.NewEncoder(db.file)
err = enc.Encode(db)
if err != nil {
return er("Encoding", err)
}
err = db.file.Close()
if err != nil {
return er("Closing file", err)
}
return nil
}
func (db *DB) Add(name string) error {
if _, ok := db.CommitList[name]; ok {
return fmt.Errorf("%s already in the list", name)
}
db.CommitList[name] = true
return nil
}
func (db *DB) Rem(name string) error {
if _, ok := db.CommitList[name]; !ok {
return fmt.Errorf("%s wasn't in the list", name)
}
delete(db.CommitList, name)
return nil
}
func (db *DB) Com() error {
er := ger("")
err := os.MkdirAll(db.dirname, os.ModeDir|0777)
db.ID++
if err != nil {
return er("Mkdir", err)
}
for name := range db.CommitList {
filea, err := os.Open(name)
if err != nil {
return er("Opening committed file", err)
}
defer filea.Close()
content, err := ioutil.ReadAll(filea)
if err != nil {
return er("Reading committed file", err)
}
filename := db.name(name, db.ID)
fileb, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return err
}
defer fileb.Close()
_, err = fileb.Write(content)
if err != nil {
return err
}
filea.Close()
fileb.Close()
}
return nil
}
func (db *DB) name(filename string, id int) string {
return fmt.Sprintf("%s/%s_%d.min", db.dirname, filename, id)
}
func (db *DB) Rev(id int) error {
er := ger("")
if len(db.CommitList) == 0 {
return er("Nothing yet", nil)
}
for name := range db.CommitList {
filea, err := os.Open(db.name(name, id))
if err != nil {
continue
}
defer filea.Close()
for i := id + 1; i <= db.ID; i++ {
fmt.Println("deleting", db.name(name, i))
err := os.Remove(db.name(name, i))
if err != nil {
fmt.Println(err)
}
}
content, err := ioutil.ReadAll(filea)
if err != nil {
return er("Reading committed file", err)
}
fileb, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return err
}
defer fileb.Close()
_, err = fileb.Write(content)
if err != nil {
return err
}
}
db.ID -= id
return nil
}
func (db *DB) List() error {
i := 0
fmt.Println("Last rev :", db.ID)
for name := range db.CommitList {
fmt.Printf("%d - %s\n", i, name)
i++
}
return nil
}
var missArg error = errors.New("Wrong number of argument")
var wrongCmd error = errors.New("Wrong command")
func HandleCmd(db *DB) error {
er := ger("Handling cmd")
numArgs := len(os.Args) - 1
cmd := os.Args[1]
var name string
if numArgs == 2 {
name = os.Args[2]
}
switch cmd {
case "add":
if numArgs != 2 {
return missArg
}
err := db.Add(name)
if err != nil {
return er("Add", err)
}
return db.Write()
case "rem":
if numArgs != 2 {
return missArg
}
err := db.Rem(name)
if err != nil {
return er("Rem", err)
}
return db.Write()
case "com":
err := db.Com()
if err != nil {
return er("Com", err)
}
return db.Write()
case "rev":
id, err := strconv.Atoi(name)
if err != nil {
return er("Rev", err)
}
return db.Rev(id)
case "list":
return db.List()
default:
return wrongCmd
}
return nil
}
func main() {
numArgs := len(os.Args) - 1
if numArgs < 1 || numArgs > 2 {
printHelp()
os.Exit(0)
}
filename := "commitfile"
dirname := "commitdir"
db, err := LoadDB(filename, dirname)
if err != nil {
log.Fatal(err)
}
defer db.file.Close()
err = HandleCmd(db)
if err == missArg || err == wrongCmd {
fmt.Println(err)
printHelp()
} else if err != nil {
log.Println(err)
}
}
func printHelp() {
fmt.Printf("MVCS - help - Commands :\n" +
"\tadd [filename]\n" +
"\trem [filename]\n" +
"\tcom\n" +
"\trev [rev numb]\n" +
"\tlist\n")
}
func ger(ctxt string) func(string, error) error {
return func(msg string, err error) error {
return fmt.Errorf("%s : %s : %v", ctxt, msg, err)
}
}第一次在Go中使用某种可用的CLI工具时,我真的不知道git是如何工作的(仍然不知道),所以我可能在里面误用了单词。
我希望在错误处理方面有所改进,并且可能会发现一种更好的方法来执行这种程序。也许有些东西不是惯用的,我想知道。
发布于 2015-08-18 07:13:49
你的“版本控制系统”原则对我来说有点奇怪。
您不存储属于修订版(ID)的文件,因此,例如,如果您添加一个文件并在稍后删除它,则db文件将不了解它。因此,如果执行还原操作,它将不会删除该文件先前提交的实例。
另外,由于每次提交都会复制版本控制下的所有文件,因此似乎没有必要将id添加到每个文件名中。只要在提交文件夹中创建一个以修订或ID命名的子文件夹,然后在不重命名的情况下复制该文件夹下的文件,将会更容易和更干净。
这个ID-子文件夹实现为您提供了比db文件更多的信息:它为每个版本提供了文件列表,而db文件可以用来提供“阶段性”提交的文件列表。任意ID子文件夹的文件夹列表给出了文件列表,文件夹名称给出了ID,最大的ID给出了最新的提交。当然,如果要改进应用程序来处理注释,可以在每个ID子文件夹中使用db或meta文件。
在进行还原("rev")时,代码会从具有较高版本的提交文件夹中删除文件。这是一个非常不正常和奇怪的实现,它应该只在本地文件夹中复制修订并保持提交文件夹不变。
如果您想认真对待您的版本控制工具,我建议您首先了解现有的流行版本控制系统,以获取想法,并了解哪些是可行的,哪些是不可行的。
很难跟踪存储在DB结构中的文件何时关闭以及是否关闭。LoadDB函数不会关闭它。如果您创建的资源是返回的,而不是关闭的,那么您应该在它上提供一个Close()方法,它向用户(可能只有您)清楚地表明,它有应该正确关闭的资源。
例如,如果在LoadDB()中打开文件成功,但读取和解码其内容失败,则LoadDB()将返回一个错误并没有关闭它。您的main()函数在这个calse中调用了LoadDB(),它将执行一个log.Fatal()调用,而不是关闭它,因为defer db.file.Close()只在此之后被调用。虽然这不会造成任何问题,但有时适当关闭资源是不太好的,而其他时候则让资源挂起,并将其交给操作系统发布。
要解决这个问题,只需删除DB.file字段(我不认为有什么用途--无论是性能方面的还是其他方面),并在需要时/在哪里打开它(并使用defer Close())。或者--如前所述--提供一个DB.Close()方法,并在main()函数中使用defer db.Close()。
DB.Com()方法:每当我需要使用defer做一些事情时,我都会在一个单独的函数中做一些事情,因此很清楚defer何时需要运行以及它所做的事情。并且使延迟函数能够尽快运行,不再需要资源时保留资源。在您的Com()方法中,您在循环中使用defer,并在每次迭代结束时手动执行。即使您不想将代码分离(到一个不同的函数),我至少要使用一个匿名函数,它将指定调用中使用的defers的点,因此您也可以避免重复,例如:
for name := range something {
func() {
res, err := ... // Open some resource
if err != nil {
// Handle error
}
defer res.Close()
// Do something with res
}()
}当然,这样看起来更好:
func handleRes(name string) error {
res, err := ... // Open some resource
if err != nil {
return err
}
defer res.Close()
// Do something with res...
return nil
}()
for name := range something {
handleRes(name)
}还请注意,当您将文件路径附加到提交dir时,您的代码在Windows系统上不能正常工作,如果文件路径包含驱动器号(例如C:\my\file.txt),则连接将导致无效路径。解决此问题的一个(不一定是最好的)解决方案可能是在提交dir中引入一个“驱动器”级别,例如,C:\my\file.txt将转到"commitdir/c/my/file.txt"。这是非常容易实现的,首先将文件转换为绝对,然后删除':'字符,然后像以前一样继续连接。
还请注意,DB.Rev()方法在试图复制文件时不会创建子目录,如果不存在子文件夹,则会出现错误。您还应该在复制之前使用os.MkdirAll()。
在DB.Com()方法中,通过将文件的全部内容读入内存(使用ioutil.ReadAll())来复制文件,然后将内容从内存写入目标文件。这是低效的,在大文件的情况下变得非常不可行。由于*os.File实现了io.Reader和io.Writer接口,所以您可以简单地使用io.Copy()函数将文件的内容复制到另一个文件中,例如:
// filea and fileb are opened:
if _, err := io.Copy(fileb, filea); err != nil {
// Failed to copy
}要打开文件(仅用于读取),您只需使用更短、更清晰的os.Open()函数即可。为了编写(仅)目的(如果存在,可以截断),可以使用更短、更清晰的os.Create()函数。
我认为您的错误处理很好,在某些方面也很有创造性( ger()函数返回另一个带有上下文的函数)。
https://codereview.stackexchange.com/questions/100525
复制相似问题