首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >最小版本控制系统

最小版本控制系统
EN

Code Review用户
提问于 2015-08-10 21:05:25
回答 1查看 426关注 0票数 11

我刚写完一个简单的版本控制。使用"add“注册文件,然后"com”将它们保存在一个目录中,并附加一个ID,所有文件的ID相同。使用"rev“,它将复制文件的内容,但不会删除除"commitdir”中的文件之外的任何其他文件。

代码语言:javascript
复制
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是如何工作的(仍然不知道),所以我可能在里面误用了单词。

我希望在错误处理方面有所改进,并且可能会发现一种更好的方法来执行这种程序。也许有些东西不是惯用的,我想知道。

EN

回答 1

Code Review用户

回答已采纳

发布于 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的点,因此您也可以避免重复,例如:

代码语言:javascript
复制
for name := range something {
    func() {
        res, err := ... // Open some resource
        if err != nil {
            // Handle error
        }
        defer res.Close()
        // Do something with res
    }()
}

当然,这样看起来更好:

代码语言:javascript
复制
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.Readerio.Writer接口,所以您可以简单地使用io.Copy()函数将文件的内容复制到另一个文件中,例如:

代码语言:javascript
复制
// filea and fileb are opened:
if _, err := io.Copy(fileb, filea); err != nil {
    // Failed to copy
}

从文件读取和写入文件:

要打开文件(仅用于读取),您只需使用更短、更清晰的os.Open()函数即可。为了编写(仅)目的(如果存在,可以截断),可以使用更短、更清晰的os.Create()函数。

错误处理

我认为您的错误处理很好,在某些方面也很有创造性( ger()函数返回另一个带有上下文的函数)。

有关其他创造性错误处理,请参阅这个答案这个问题的答案。

票数 6
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/100525

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档