diff options
author | Michael Muré <batolettre@gmail.com> | 2018-07-12 09:55:13 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2018-07-12 09:55:13 +0200 |
commit | d0443659123f912e9385e27efebe4b7da65aa2f6 (patch) | |
tree | 090a36d2618ecff259cbcabd501d33d460d1cd7a | |
parent | a85730cf5287d40a1e32d3a671ba2296c73387cb (diff) | |
download | git-bug-d0443659123f912e9385e27efebe4b7da65aa2f6.tar.gz |
more experiment
-rw-r--r-- | commands/commands.go | 40 | ||||
-rw-r--r-- | commands/pull.go | 33 | ||||
-rw-r--r-- | commands/push.go | 33 | ||||
-rw-r--r-- | git-bug.go | 72 | ||||
-rw-r--r-- | notes | 47 | ||||
-rw-r--r-- | repository/git.go | 838 | ||||
-rw-r--r-- | repository/mock_repo.go | 33 | ||||
-rw-r--r-- | repository/repo.go | 20 |
8 files changed, 1114 insertions, 2 deletions
diff --git a/commands/commands.go b/commands/commands.go new file mode 100644 index 00000000..4f892fdd --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,40 @@ +// Package commands contains the assorted sub commands supported by the git-bug tool. +package commands + +import ( + "github.com/MichaelMure/git-bug/repository" +) + +const bugsRefPattern = "refs/bugs/*" + +// Command represents the definition of a single command. +type Command struct { + Usage func(string) + RunMethod func(repository.Repo, []string) error +} + +// Run executes a command, given its arguments. +// +// The args parameter is all of the command line args that followed the +// subcommand. +func (cmd *Command) Run(repo repository.Repo, args []string) error { + return cmd.RunMethod(repo, args) +} + +// CommandMap defines all of the available (sub)commands. +var CommandMap = map[string]*Command{ + "pull": pullCmd, + "push": pushCmd, + + /*"abandon": abandonCmd, + "accept": acceptCmd, + "comment": commentCmd, + "list": listCmd, + "pull": pullCmd, + "push": pushCmd, + "rebase": rebaseCmd, + "reject": rejectCmd, + "request": requestCmd, + "show": showCmd, + "submit": submitCmd,*/ +} diff --git a/commands/pull.go b/commands/pull.go new file mode 100644 index 00000000..0f6080d4 --- /dev/null +++ b/commands/pull.go @@ -0,0 +1,33 @@ +package commands + +import ( + "fmt" + "github.com/MichaelMure/git-bug/repository" + "errors" +) + +func pull(repo repository.Repo, args []string) error { + if len(args) > 1 { + return errors.New("only pulling from one remote at a time is supported") + } + + remote := "origin" + if len(args) == 1 { + remote = args[0] + } + + if err := repo.PullRefs(remote, bugsRefPattern); err != nil { + return err + } + return nil +} + +// showCmd defines the "push" subcommand. +var pullCmd = &Command{ + Usage: func(arg0 string) { + fmt.Printf("Usage: %s pull [<remote>]\n", arg0) + }, + RunMethod: func(repo repository.Repo, args []string) error { + return pull(repo, args) + }, +}
\ No newline at end of file diff --git a/commands/push.go b/commands/push.go new file mode 100644 index 00000000..4465f561 --- /dev/null +++ b/commands/push.go @@ -0,0 +1,33 @@ +package commands + +import ( + "fmt" + "github.com/MichaelMure/git-bug/repository" + "errors" +) + +func push(repo repository.Repo, args []string) error { + if len(args) > 1 { + return errors.New("only pushing to one remote at a time is supported") + } + + remote := "origin" + if len(args) == 1 { + remote = args[0] + } + + if err := repo.PushRefs(remote, bugsRefPattern); err != nil { + return err + } + return nil +} + +// showCmd defines the "push" subcommand. +var pushCmd = &Command{ + Usage: func(arg0 string) { + fmt.Printf("Usage: %s push [<remote>]\n", arg0) + }, + RunMethod: func(repo repository.Repo, args []string) error { + return push(repo, args) + }, +}
\ No newline at end of file @@ -1,7 +1,75 @@ package main -import "fmt" +import ( + "fmt" + "os" + "sort" + "strings" + "github.com/MichaelMure/git-bug/repository" + "github.com/MichaelMure/git-bug/commands" +) + +const usageMessageTemplate = `Usage: %s <command> + +Where <command> is one of: + %s + +For individual command usage, run: + %s help <command> +` + +func usage() { + command := os.Args[0] + var subcommands []string + for subcommand := range commands.CommandMap { + subcommands = append(subcommands, subcommand) + } + sort.Strings(subcommands) + fmt.Printf(usageMessageTemplate, command, strings.Join(subcommands, "\n "), command) +} + +func help() { + if len(os.Args) < 3 { + usage() + return + } + subcommand, ok := commands.CommandMap[os.Args[2]] + if !ok { + fmt.Printf("Unknown command %q\n", os.Args[2]) + usage() + return + } + subcommand.Usage(os.Args[0]) +} func main() { - fmt.Println("Hello git !") + if len(os.Args) > 1 && os.Args[1] == "help" { + help() + return + } + cwd, err := os.Getwd() + if err != nil { + fmt.Printf("Unable to get the current working directory: %q\n", err) + return + } + repo, err := repository.NewGitRepo(cwd) + if err != nil { + fmt.Printf("%s must be run from within a git repo.\n", os.Args[0]) + return + } + if len(os.Args) < 2 { + // default behavior + fmt.Println("Not implemented") + return + } + subcommand, ok := commands.CommandMap[os.Args[1]] + if !ok { + fmt.Printf("Unknown command: %q\n", os.Args[1]) + usage() + return + } + if err := subcommand.Run(repo, os.Args[2:]); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } } @@ -0,0 +1,47 @@ +echo "dnkqslnokqe" | git hash-object --stdin -w +bca2068e9b221d47154d1e3eaebf8c9abd86951f + +git cat-file -p bca2068e9b221d47154d1e3eaebf8c9abd86951f +dnkqslnokqe + + +git update-ref refs/bug/HEAD bca2068e9b221d47154d1e3eaebf8c9abd86951f + +git show refs/bug/HEAD + +git push origin "refs/bug/*" + +git fetch origin "refs/bug/*:refs/bug/*" + + + + + + +Bug operations: +- create bug +- change title +- add comment +- edit text comment +- hide/unhide/delete comment +- (?) reply to comment +- add/remove [label, assignee, project, milestone, ...] + + +G-Set (append only log) +----- + +- create bug +- change title +- add/edit/hide/unhide/delete comment + + +LWW-e-Set (add + remove, based on timestamp) +--------- + +- label +- assignee +- project +- milestone + + diff --git a/repository/git.go b/repository/git.go new file mode 100644 index 00000000..8e957523 --- /dev/null +++ b/repository/git.go @@ -0,0 +1,838 @@ +// Package repository contains helper methods for working with the Git repo. +package repository + +import ( + "bytes" + "crypto/sha1" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +const branchRefPrefix = "refs/heads/" + +// GitRepo represents an instance of a (local) git repository. +type GitRepo struct { + Path string +} + +// Run the given git command with the given I/O reader/writers, returning an error if it fails. +func (repo *GitRepo) runGitCommandWithIO(stdin io.Reader, stdout, stderr io.Writer, args ...string) error { + cmd := exec.Command("git", args...) + cmd.Dir = repo.Path + cmd.Stdin = stdin + cmd.Stdout = stdout + cmd.Stderr = stderr + return cmd.Run() +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (repo *GitRepo) runGitCommandRaw(args ...string) (string, string, error) { + var stdout bytes.Buffer + var stderr bytes.Buffer + err := repo.runGitCommandWithIO(nil, &stdout, &stderr, args...) + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), err +} + +// Run the given git command and return its stdout, or an error if the command fails. +func (repo *GitRepo) runGitCommand(args ...string) (string, error) { + stdout, stderr, err := repo.runGitCommandRaw(args...) + if err != nil { + if stderr == "" { + stderr = "Error running git command: " + strings.Join(args, " ") + } + err = fmt.Errorf(stderr) + } + return stdout, err +} + +// Run the given git command using the same stdin, stdout, and stderr as the review tool. +func (repo *GitRepo) runGitCommandInline(args ...string) error { + return repo.runGitCommandWithIO(os.Stdin, os.Stdout, os.Stderr, args...) +} + +// NewGitRepo determines if the given working directory is inside of a git repository, +// and returns the corresponding GitRepo instance if it is. +func NewGitRepo(path string) (*GitRepo, error) { + repo := &GitRepo{Path: path} + _, _, err := repo.runGitCommandRaw("rev-parse") + if err == nil { + return repo, nil + } + if _, ok := err.(*exec.ExitError); ok { + return nil, err + } + return nil, err +} + +// GetPath returns the path to the repo. +func (repo *GitRepo) GetPath() string { + return repo.Path +} + +// GetRepoStateHash returns a hash which embodies the entire current state of a repository. +func (repo *GitRepo) GetRepoStateHash() (string, error) { + stateSummary, err := repo.runGitCommand("show-ref") + return fmt.Sprintf("%x", sha1.Sum([]byte(stateSummary))), err +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (repo *GitRepo) GetUserEmail() (string, error) { + return repo.runGitCommand("config", "user.email") +} + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (repo *GitRepo) GetCoreEditor() (string, error) { + return repo.runGitCommand("var", "GIT_EDITOR") +} + + +// PullRefs pull git refs from a remote +func (repo *GitRepo) PullRefs(remote string, refPattern string) error { + refspec := fmt.Sprintf("%s:%s", refPattern, refPattern) + + // The push is liable to fail if the user forgot to do a pull first, so + // we treat errors as user errors rather than fatal errors. + err := repo.runGitCommandInline("push", remote, refspec) + if err != nil { + return fmt.Errorf("failed to push to the remote '%s': %v", remote, err) + } + return nil +} + +// PushRefs push git refs to a remote +func (repo *GitRepo) PushRefs(remote string, refPattern string) error { + remoteNotesRefPattern := getRemoteNotesRef(remote, refPattern) + fetchRefSpec := fmt.Sprintf("+%s:%s", refPattern, remoteNotesRefPattern) + err := repo.runGitCommandInline("fetch", remote, fetchRefSpec) + if err != nil { + return err + } + + return repo.mergeRemoteNotes(remote, notesRefPattern) +} + +/* +// +//// GetSubmitStrategy returns the way in which a review is submitted +//func (repo *GitRepo) GetSubmitStrategy() (string, error) { +// submitStrategy, _ := repo.runGitCommand("config", "appraise.submit") +// return submitStrategy, nil +//} +// +//// HasUncommittedChanges returns true if there are local, uncommitted changes. +//func (repo *GitRepo) HasUncommittedChanges() (bool, error) { +// out, err := repo.runGitCommand("status", "--porcelain") +// if err != nil { +// return false, err +// } +// if len(out) > 0 { +// return true, nil +// } +// return false, nil +//} +// +//// VerifyCommit verifies that the supplied hash points to a known commit. +//func (repo *GitRepo) VerifyCommit(hash string) error { +// out, err := repo.runGitCommand("cat-file", "-t", hash) +// if err != nil { +// return err +// } +// objectType := strings.TrimSpace(string(out)) +// if objectType != "commit" { +// return fmt.Errorf("Hash %q points to a non-commit object of type %q", hash, objectType) +// } +// return nil +//} +// +//// VerifyGitRef verifies that the supplied ref points to a known commit. +//func (repo *GitRepo) VerifyGitRef(ref string) error { +// _, err := repo.runGitCommand("show-ref", "--verify", ref) +// return err +//} +// +//// GetHeadRef returns the ref that is the current HEAD. +//func (repo *GitRepo) GetHeadRef() (string, error) { +// return repo.runGitCommand("symbolic-ref", "HEAD") +//} +// +//// GetCommitHash returns the hash of the commit pointed to by the given ref. +//func (repo *GitRepo) GetCommitHash(ref string) (string, error) { +// return repo.runGitCommand("show", "-s", "--format=%H", ref) +//} +// +//// ResolveRefCommit returns the commit pointed to by the given ref, which may be a remote ref. +//// +//// This differs from GetCommitHash which only works on exact matches, in that it will try to +//// intelligently handle the scenario of a ref not existing locally, but being known to exist +//// in a remote repo. +//// +//// This method should be used when a command may be performed by either the reviewer or the +//// reviewee, while GetCommitHash should be used when the encompassing command should only be +//// performed by the reviewee. +//func (repo *GitRepo) ResolveRefCommit(ref string) (string, error) { +// if err := repo.VerifyGitRef(ref); err == nil { +// return repo.GetCommitHash(ref) +// } +// if strings.HasPrefix(ref, "refs/heads/") { +// // The ref is a branch. Check if it exists in exactly one remote +// pattern := strings.Replace(ref, "refs/heads", "**", 1) +// matchingOutput, err := repo.runGitCommand("for-each-ref", "--format=%(refname)", pattern) +// if err != nil { +// return "", err +// } +// matchingRefs := strings.Split(matchingOutput, "\n") +// if len(matchingRefs) == 1 && matchingRefs[0] != "" { +// // There is exactly one match +// return repo.GetCommitHash(matchingRefs[0]) +// } +// return "", fmt.Errorf("Unable to find a git ref matching the pattern %q", pattern) +// } +// return "", fmt.Errorf("Unknown git ref %q", ref) +//} +// +//// GetCommitMessage returns the message stored in the commit pointed to by the given ref. +//func (repo *GitRepo) GetCommitMessage(ref string) (string, error) { +// return repo.runGitCommand("show", "-s", "--format=%B", ref) +//} +// +//// GetCommitTime returns the commit time of the commit pointed to by the given ref. +//func (repo *GitRepo) GetCommitTime(ref string) (string, error) { +// return repo.runGitCommand("show", "-s", "--format=%ct", ref) +//} +// +//// GetLastParent returns the last parent of the given commit (as ordered by git). +//func (repo *GitRepo) GetLastParent(ref string) (string, error) { +// return repo.runGitCommand("rev-list", "--skip", "1", "-n", "1", ref) +//} +// +//// GetCommitDetails returns the details of a commit's metadata. +//func (repo GitRepo) GetCommitDetails(ref string) (*CommitDetails, error) { +// var err error +// show := func(formatString string) (result string) { +// if err != nil { +// return "" +// } +// result, err = repo.runGitCommand("show", "-s", ref, fmt.Sprintf("--format=tformat:%s", formatString)) +// return result +// } +// +// jsonFormatString := "{\"tree\":\"%T\", \"time\": \"%at\"}" +// detailsJSON := show(jsonFormatString) +// if err != nil { +// return nil, err +// } +// var details CommitDetails +// err = json.Unmarshal([]byte(detailsJSON), &details) +// if err != nil { +// return nil, err +// } +// details.Author = show("%an") +// details.AuthorEmail = show("%ae") +// details.Summary = show("%s") +// parentsString := show("%P") +// details.Parents = strings.Split(parentsString, " ") +// if err != nil { +// return nil, err +// } +// return &details, nil +//} +// +//// MergeBase determines if the first commit that is an ancestor of the two arguments. +//func (repo *GitRepo) MergeBase(a, b string) (string, error) { +// return repo.runGitCommand("merge-base", a, b) +//} +// +//// IsAncestor determines if the first argument points to a commit that is an ancestor of the second. +//func (repo *GitRepo) IsAncestor(ancestor, descendant string) (bool, error) { +// _, _, err := repo.runGitCommandRaw("merge-base", "--is-ancestor", ancestor, descendant) +// if err == nil { +// return true, nil +// } +// if _, ok := err.(*exec.ExitError); ok { +// return false, nil +// } +// return false, fmt.Errorf("Error while trying to determine commit ancestry: %v", err) +//} +// +//// Diff computes the diff between two given commits. +//func (repo *GitRepo) Diff(left, right string, diffArgs ...string) (string, error) { +// args := []string{"diff"} +// args = append(args, diffArgs...) +// args = append(args, fmt.Sprintf("%s..%s", left, right)) +// return repo.runGitCommand(args...) +//} +// +//// Show returns the contents of the given file at the given commit. +//func (repo *GitRepo) Show(commit, path string) (string, error) { +// return repo.runGitCommand("show", fmt.Sprintf("%s:%s", commit, path)) +//} +// +//// SwitchToRef changes the currently-checked-out ref. +//func (repo *GitRepo) SwitchToRef(ref string) error { +// // If the ref starts with "refs/heads/", then we have to trim that prefix, +// // or else we will wind up in a detached HEAD state. +// if strings.HasPrefix(ref, branchRefPrefix) { +// ref = ref[len(branchRefPrefix):] +// } +// _, err := repo.runGitCommand("checkout", ref) +// return err +//} +// +//// mergeArchives merges two archive refs. +//func (repo *GitRepo) mergeArchives(archive, remoteArchive string) error { +// remoteHash, err := repo.GetCommitHash(remoteArchive) +// if err != nil { +// return err +// } +// if remoteHash == "" { +// // The remote archive does not exist, so we have nothing to do +// return nil +// } +// +// archiveHash, err := repo.GetCommitHash(archive) +// if err != nil { +// return err +// } +// if archiveHash == "" { +// // The local archive does not exist, so we merely need to set it +// _, err := repo.runGitCommand("update-ref", archive, remoteHash) +// return err +// } +// +// isAncestor, err := repo.IsAncestor(archiveHash, remoteHash) +// if err != nil { +// return err +// } +// if isAncestor { +// // The archive can simply be fast-forwarded +// _, err := repo.runGitCommand("update-ref", archive, remoteHash, archiveHash) +// return err +// } +// +// // Create a merge commit of the two archives +// refDetails, err := repo.GetCommitDetails(remoteArchive) +// if err != nil { +// return err +// } +// newArchiveHash, err := repo.runGitCommand("commit-tree", "-p", remoteHash, "-p", archiveHash, "-m", "Merge local and remote archives", refDetails.Tree) +// if err != nil { +// return err +// } +// newArchiveHash = strings.TrimSpace(newArchiveHash) +// _, err = repo.runGitCommand("update-ref", archive, newArchiveHash, archiveHash) +// return err +//} +// +//// ArchiveRef adds the current commit pointed to by the 'ref' argument +//// under the ref specified in the 'archive' argument. +//// +//// Both the 'ref' and 'archive' arguments are expected to be the fully +//// qualified names of git refs (e.g. 'refs/heads/my-change' or +//// 'refs/devtools/archives/reviews'). +//// +//// If the ref pointed to by the 'archive' argument does not exist +//// yet, then it will be created. +//func (repo *GitRepo) ArchiveRef(ref, archive string) error { +// refHash, err := repo.GetCommitHash(ref) +// if err != nil { +// return err +// } +// refDetails, err := repo.GetCommitDetails(ref) +// if err != nil { +// return err +// } +// +// commitTreeArgs := []string{"commit-tree"} +// archiveHash, err := repo.GetCommitHash(archive) +// if err != nil { +// archiveHash = "" +// } else { +// commitTreeArgs = append(commitTreeArgs, "-p", archiveHash) +// } +// commitTreeArgs = append(commitTreeArgs, "-p", refHash, "-m", fmt.Sprintf("Archive %s", refHash), refDetails.Tree) +// newArchiveHash, err := repo.runGitCommand(commitTreeArgs...) +// if err != nil { +// return err +// } +// newArchiveHash = strings.TrimSpace(newArchiveHash) +// updateRefArgs := []string{"update-ref", archive, newArchiveHash} +// if archiveHash != "" { +// updateRefArgs = append(updateRefArgs, archiveHash) +// } +// _, err = repo.runGitCommand(updateRefArgs...) +// return err +//} +// +//// MergeRef merges the given ref into the current one. +//// +//// The ref argument is the ref to merge, and fastForward indicates that the +//// current ref should only move forward, as opposed to creating a bubble merge. +//// The messages argument(s) provide text that should be included in the default +//// merge commit message (separated by blank lines). +//func (repo *GitRepo) MergeRef(ref string, fastForward bool, messages ...string) error { +// args := []string{"merge"} +// if fastForward { +// args = append(args, "--ff", "--ff-only") +// } else { +// args = append(args, "--no-ff") +// } +// if len(messages) > 0 { +// commitMessage := strings.Join(messages, "\n\n") +// args = append(args, "-e", "-m", commitMessage) +// } +// args = append(args, ref) +// return repo.runGitCommandInline(args...) +//} +// +//// RebaseRef rebases the current ref onto the given one. +//func (repo *GitRepo) RebaseRef(ref string) error { +// return repo.runGitCommandInline("rebase", "-i", ref) +//} +// +//// ListCommits returns the list of commits reachable from the given ref. +//// +//// The generated list is in chronological order (with the oldest commit first). +//// +//// If the specified ref does not exist, then this method returns an empty result. +//func (repo *GitRepo) ListCommits(ref string) []string { +// var stdout bytes.Buffer +// var stderr bytes.Buffer +// if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "rev-list", "--reverse", ref); err != nil { +// return nil +// } +// +// byteLines := bytes.Split(stdout.Bytes(), []byte("\n")) +// var commits []string +// for _, byteLine := range byteLines { +// commits = append(commits, string(byteLine)) +// } +// return commits +//} +// +//// ListCommitsBetween returns the list of commits between the two given revisions. +//// +//// The "from" parameter is the starting point (exclusive), and the "to" +//// parameter is the ending point (inclusive). +//// +//// The "from" commit does not need to be an ancestor of the "to" commit. If it +//// is not, then the merge base of the two is used as the starting point. +//// Admittedly, this makes calling these the "between" commits is a bit of a +//// misnomer, but it also makes the method easier to use when you want to +//// generate the list of changes in a feature branch, as it eliminates the need +//// to explicitly calculate the merge base. This also makes the semantics of the +//// method compatible with git's built-in "rev-list" command. +//// +//// The generated list is in chronological order (with the oldest commit first). +//func (repo *GitRepo) ListCommitsBetween(from, to string) ([]string, error) { +// out, err := repo.runGitCommand("rev-list", "--reverse", from+".."+to) +// if err != nil { +// return nil, err +// } +// if out == "" { +// return nil, nil +// } +// return strings.Split(out, "\n"), nil +//} +// +//// GetNotes uses the "git" command-line tool to read the notes from the given ref for a given revision. +//func (repo *GitRepo) GetNotes(notesRef, revision string) []Note { +// var notes []Note +// rawNotes, err := repo.runGitCommand("notes", "--ref", notesRef, "show", revision) +// if err != nil { +// // We just assume that this means there are no notes +// return nil +// } +// for _, line := range strings.Split(rawNotes, "\n") { +// notes = append(notes, Note([]byte(line))) +// } +// return notes +//} +// +//func stringsReader(s []*string) io.Reader { +// var subReaders []io.Reader +// for _, strPtr := range s { +// subReader := strings.NewReader(*strPtr) +// subReaders = append(subReaders, subReader, strings.NewReader("\n")) +// } +// return io.MultiReader(subReaders...) +//} +// +//// splitBatchCheckOutput parses the output of a 'git cat-file --batch-check=...' command. +//// +//// The output is expected to be formatted as a series of entries, with each +//// entry consisting of: +//// 1. The SHA1 hash of the git object being output, followed by a space. +//// 2. The git "type" of the object (commit, blob, tree, missing, etc), followed by a newline. +//// +//// To generate this format, make sure that the 'git cat-file' command includes +//// the argument '--batch-check=%(objectname) %(objecttype)'. +//// +//// The return value is a map from object hash to a boolean indicating if that object is a commit. +//func splitBatchCheckOutput(out *bytes.Buffer) (map[string]bool, error) { +// isCommit := make(map[string]bool) +// reader := bufio.NewReader(out) +// for { +// nameLine, err := reader.ReadString(byte(' ')) +// if err == io.EOF { +// return isCommit, nil +// } +// if err != nil { +// return nil, fmt.Errorf("Failure while reading the next object name: %v", err) +// } +// nameLine = strings.TrimSuffix(nameLine, " ") +// typeLine, err := reader.ReadString(byte('\n')) +// if err != nil && err != io.EOF { +// return nil, fmt.Errorf("Failure while reading the next object type: %q - %v", nameLine, err) +// } +// typeLine = strings.TrimSuffix(typeLine, "\n") +// if typeLine == "commit" { +// isCommit[nameLine] = true +// } +// } +//} +// +//// splitBatchCatFileOutput parses the output of a 'git cat-file --batch=...' command. +//// +//// The output is expected to be formatted as a series of entries, with each +//// entry consisting of: +//// 1. The SHA1 hash of the git object being output, followed by a newline. +//// 2. The size of the object's contents in bytes, followed by a newline. +//// 3. The objects contents. +//// +//// To generate this format, make sure that the 'git cat-file' command includes +//// the argument '--batch=%(objectname)\n%(objectsize)'. +//func splitBatchCatFileOutput(out *bytes.Buffer) (map[string][]byte, error) { +// contentsMap := make(map[string][]byte) +// reader := bufio.NewReader(out) +// for { +// nameLine, err := reader.ReadString(byte('\n')) +// if strings.HasSuffix(nameLine, "\n") { +// nameLine = strings.TrimSuffix(nameLine, "\n") +// } +// if err == io.EOF { +// return contentsMap, nil +// } +// if err != nil { +// return nil, fmt.Errorf("Failure while reading the next object name: %v", err) +// } +// sizeLine, err := reader.ReadString(byte('\n')) +// if strings.HasSuffix(sizeLine, "\n") { +// sizeLine = strings.TrimSuffix(sizeLine, "\n") +// } +// if err != nil { +// return nil, fmt.Errorf("Failure while reading the next object size: %q - %v", nameLine, err) +// } +// size, err := strconv.Atoi(sizeLine) +// if err != nil { +// return nil, fmt.Errorf("Failure while parsing the next object size: %q - %v", nameLine, err) +// } +// contentBytes := make([]byte, size, size) +// readDest := contentBytes +// len := 0 +// err = nil +// for err == nil && len < size { +// nextLen := 0 +// nextLen, err = reader.Read(readDest) +// len += nextLen +// readDest = contentBytes[len:] +// } +// contentsMap[nameLine] = contentBytes +// if err == io.EOF { +// return contentsMap, nil +// } +// if err != nil { +// return nil, err +// } +// for bs, err := reader.Peek(1); err == nil && bs[0] == byte('\n'); bs, err = reader.Peek(1) { +// reader.ReadByte() +// } +// } +//} +// +//// notesMapping represents the association between a git object and the notes for that object. +//type notesMapping struct { +// ObjectHash *string +// NotesHash *string +//} +// +//// notesOverview represents a high-level overview of all the notes under a single notes ref. +//type notesOverview struct { +// NotesMappings []*notesMapping +// ObjectHashesReader io.Reader +// NotesHashesReader io.Reader +//} +// +//// notesOverview returns an overview of the git notes stored under the given ref. +//func (repo *GitRepo) notesOverview(notesRef string) (*notesOverview, error) { +// var stdout bytes.Buffer +// var stderr bytes.Buffer +// if err := repo.runGitCommandWithIO(nil, &stdout, &stderr, "notes", "--ref", notesRef, "list"); err != nil { +// return nil, err +// } +// +// var notesMappings []*notesMapping +// var objHashes []*string +// var notesHashes []*string +// outScanner := bufio.NewScanner(&stdout) +// for outScanner.Scan() { +// line := outScanner.Text() +// lineParts := strings.Split(line, " ") +// if len(lineParts) != 2 { +// return nil, fmt.Errorf("Malformed output line from 'git-notes list': %q", line) +// } +// objHash := &lineParts[1] +// notesHash := &lineParts[0] +// notesMappings = append(notesMappings, ¬esMapping{ +// ObjectHash: objHash, +// NotesHash: notesHash, +// }) +// objHashes = append(objHashes, objHash) +// notesHashes = append(notesHashes, notesHash) +// } +// err := outScanner.Err() +// if err != nil && err != io.EOF { +// return nil, fmt.Errorf("Failure parsing the output of 'git-notes list': %v", err) +// } +// return ¬esOverview{ +// NotesMappings: notesMappings, +// ObjectHashesReader: stringsReader(objHashes), +// NotesHashesReader: stringsReader(notesHashes), +// }, nil +//} +// +//// getIsCommitMap returns a mapping of all the annotated objects that are commits. +//func (overview *notesOverview) getIsCommitMap(repo *GitRepo) (map[string]bool, error) { +// var stdout bytes.Buffer +// var stderr bytes.Buffer +// if err := repo.runGitCommandWithIO(overview.ObjectHashesReader, &stdout, &stderr, "cat-file", "--batch-check=%(objectname) %(objecttype)"); err != nil { +// return nil, fmt.Errorf("Failure performing a batch file check: %v", err) +// } +// isCommit, err := splitBatchCheckOutput(&stdout) +// if err != nil { +// return nil, fmt.Errorf("Failure parsing the output of a batch file check: %v", err) +// } +// return isCommit, nil +//} +// +//// getNoteContentsMap returns a mapping from all the notes hashes to their contents. +//func (overview *notesOverview) getNoteContentsMap(repo *GitRepo) (map[string][]byte, error) { +// var stdout bytes.Buffer +// var stderr bytes.Buffer +// if err := repo.runGitCommandWithIO(overview.NotesHashesReader, &stdout, &stderr, "cat-file", "--batch=%(objectname)\n%(objectsize)"); err != nil { +// return nil, fmt.Errorf("Failure performing a batch file read: %v", err) +// } +// noteContentsMap, err := splitBatchCatFileOutput(&stdout) +// if err != nil { +// return nil, fmt.Errorf("Failure parsing the output of a batch file read: %v", err) +// } +// return noteContentsMap, nil +//} +// +//// GetAllNotes reads the contents of the notes under the given ref for every commit. +//// +//// The returned value is a mapping from commit hash to the list of notes for that commit. +//// +//// This is the batch version of the corresponding GetNotes(...) method. +//func (repo *GitRepo) GetAllNotes(notesRef string) (map[string][]Note, error) { +// // This code is unfortunately quite complicated, but it needs to be so. +// // +// // Conceptually, this is equivalent to: +// // result := make(map[string][]Note) +// // for _, commit := range repo.ListNotedRevisions(notesRef) { +// // result[commit] = repo.GetNotes(notesRef, commit) +// // } +// // return result, nil +// // +// // However, that logic would require separate executions of the 'git' +// // command for every annotated commit. For a repo with 10s of thousands +// // of reviews, that would mean calling Cmd.Run(...) 10s of thousands of +// // times. That, in turn, would take so long that the tool would be unusable. +// // +// // This method avoids that by taking advantage of the 'git cat-file --batch="..."' +// // command. That allows us to use a single invocation of Cmd.Run(...) to +// // inspect multiple git objects at once. +// // +// // As such, regardless of the number of reviews in a repo, we can get all +// // of the notes using a total of three invocations of Cmd.Run(...): +// // 1. One to list all the annotated objects (and their notes hash) +// // 2. A second one to filter out all of the annotated objects that are not commits. +// // 3. A final one to get the contents of all of the notes blobs. +// overview, err := repo.notesOverview(notesRef) +// if err != nil { +// return nil, err +// } +// isCommit, err := overview.getIsCommitMap(repo) +// if err != nil { +// return nil, fmt.Errorf("Failure building the set of commit objects: %v", err) +// } +// noteContentsMap, err := overview.getNoteContentsMap(repo) +// if err != nil { +// return nil, fmt.Errorf("Failure building the mapping from notes hash to contents: %v", err) +// } +// commitNotesMap := make(map[string][]Note) +// for _, notesMapping := range overview.NotesMappings { +// if !isCommit[*notesMapping.ObjectHash] { +// continue +// } +// noteBytes := noteContentsMap[*notesMapping.NotesHash] +// byteSlices := bytes.Split(noteBytes, []byte("\n")) +// var notes []Note +// for _, slice := range byteSlices { +// notes = append(notes, Note(slice)) +// } +// commitNotesMap[*notesMapping.ObjectHash] = notes +// } +// +// return commitNotesMap, nil +//} +// +//// AppendNote appends a note to a revision under the given ref. +//func (repo *GitRepo) AppendNote(notesRef, revision string, note Note) error { +// _, err := repo.runGitCommand("notes", "--ref", notesRef, "append", "-m", string(note), revision) +// return err +//} +// +//// ListNotedRevisions returns the collection of revisions that are annotated by notes in the given ref. +//func (repo *GitRepo) ListNotedRevisions(notesRef string) []string { +// var revisions []string +// notesListOut, err := repo.runGitCommand("notes", "--ref", notesRef, "list") +// if err != nil { +// return nil +// } +// notesList := strings.Split(notesListOut, "\n") +// for _, notePair := range notesList { +// noteParts := strings.SplitN(notePair, " ", 2) +// if len(noteParts) == 2 { +// objHash := noteParts[1] +// objType, err := repo.runGitCommand("cat-file", "-t", objHash) +// // If a note points to an object that we do not know about (yet), then err will not +// // be nil. We can safely just ignore those notes. +// if err == nil && objType == "commit" { +// revisions = append(revisions, objHash) +// } +// } +// } +// return revisions +//} +// +//// PushNotes pushes git notes to a remote repo. +//func (repo *GitRepo) PushNotes(remote, notesRefPattern string) error { +// refspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern) +// +// // The push is liable to fail if the user forgot to do a pull first, so +// // we treat errors as user errors rather than fatal errors. +// err := repo.runGitCommandInline("push", remote, refspec) +// if err != nil { +// return fmt.Errorf("Failed to push to the remote '%s': %v", remote, err) +// } +// return nil +//} +// +//// PushNotesAndArchive pushes the given notes and archive refs to a remote repo. +//func (repo *GitRepo) PushNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error { +// notesRefspec := fmt.Sprintf("%s:%s", notesRefPattern, notesRefPattern) +// archiveRefspec := fmt.Sprintf("%s:%s", archiveRefPattern, archiveRefPattern) +// err := repo.runGitCommandInline("push", remote, notesRefspec, archiveRefspec) +// if err != nil { +// return fmt.Errorf("Failed to push the local archive to the remote '%s': %v", remote, err) +// } +// return nil +//} +// +//func getRemoteNotesRef(remote, localNotesRef string) string { +// relativeNotesRef := strings.TrimPrefix(localNotesRef, "refs/notes/") +// return "refs/notes/" + remote + "/" + relativeNotesRef +//} +// +//func (repo *GitRepo) mergeRemoteNotes(remote, notesRefPattern string) error { +// remoteRefs, err := repo.runGitCommand("ls-remote", remote, notesRefPattern) +// if err != nil { +// return err +// } +// for _, line := range strings.Split(remoteRefs, "\n") { +// lineParts := strings.Split(line, "\t") +// if len(lineParts) == 2 { +// ref := lineParts[1] +// remoteRef := getRemoteNotesRef(remote, ref) +// _, err := repo.runGitCommand("notes", "--ref", ref, "merge", remoteRef, "-s", "cat_sort_uniq") +// if err != nil { +// return err +// } +// } +// } +// return nil +//} +// +//// PullNotes fetches the contents of the given notes ref from a remote repo, +//// and then merges them with the corresponding local notes using the +//// "cat_sort_uniq" strategy. +//func (repo *GitRepo) PullNotes(remote, notesRefPattern string) error { +// remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern) +// fetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern) +// err := repo.runGitCommandInline("fetch", remote, fetchRefSpec) +// if err != nil { +// return err +// } +// +// return repo.mergeRemoteNotes(remote, notesRefPattern) +//} +// +//func getRemoteArchiveRef(remote, archiveRefPattern string) string { +// relativeArchiveRef := strings.TrimPrefix(archiveRefPattern, "refs/devtools/archives/") +// return "refs/devtools/remoteArchives/" + remote + "/" + relativeArchiveRef +//} +// +//func (repo *GitRepo) mergeRemoteArchives(remote, archiveRefPattern string) error { +// remoteRefs, err := repo.runGitCommand("ls-remote", remote, archiveRefPattern) +// if err != nil { +// return err +// } +// for _, line := range strings.Split(remoteRefs, "\n") { +// lineParts := strings.Split(line, "\t") +// if len(lineParts) == 2 { +// ref := lineParts[1] +// remoteRef := getRemoteArchiveRef(remote, ref) +// if err := repo.mergeArchives(ref, remoteRef); err != nil { +// return err +// } +// } +// } +// return nil +//} +// +//// PullNotesAndArchive fetches the contents of the notes and archives refs from +//// a remote repo, and merges them with the corresponding local refs. +//// +//// For notes refs, we assume that every note can be automatically merged using +//// the 'cat_sort_uniq' strategy (the git-appraise schemas fit that requirement), +//// so we automatically merge the remote notes into the local notes. +//// +//// For "archive" refs, they are expected to be used solely for maintaining +//// reachability of commits that are part of the history of any reviews, +//// so we do not maintain any consistency with their tree objects. Instead, +//// we merely ensure that their history graph includes every commit that we +//// intend to keep. +//func (repo *GitRepo) PullNotesAndArchive(remote, notesRefPattern, archiveRefPattern string) error { +// remoteArchiveRef := getRemoteArchiveRef(remote, archiveRefPattern) +// archiveFetchRefSpec := fmt.Sprintf("+%s:%s", archiveRefPattern, remoteArchiveRef) +// +// remoteNotesRefPattern := getRemoteNotesRef(remote, notesRefPattern) +// notesFetchRefSpec := fmt.Sprintf("+%s:%s", notesRefPattern, remoteNotesRefPattern) +// +// err := repo.runGitCommandInline("fetch", remote, notesFetchRefSpec, archiveFetchRefSpec) +// if err != nil { +// return err +// } +// +// if err := repo.mergeRemoteNotes(remote, notesRefPattern); err != nil { +// return err +// } +// if err := repo.mergeRemoteArchives(remote, archiveRefPattern); err != nil { +// return err +// } +// return nil +//} +*/ diff --git a/repository/mock_repo.go b/repository/mock_repo.go new file mode 100644 index 00000000..8587d0da --- /dev/null +++ b/repository/mock_repo.go @@ -0,0 +1,33 @@ +package repository + +import ( + "crypto/sha1" + "encoding/json" + "fmt" +) + +// mockRepoForTest defines an instance of Repo that can be used for testing. +type mockRepoForTest struct {} + +// GetPath returns the path to the repo. +func (r *mockRepoForTest) GetPath() string { return "~/mockRepo/" } + +// GetRepoStateHash returns a hash which embodies the entire current state of a repository. +func (r *mockRepoForTest) GetRepoStateHash() (string, error) { + repoJSON, err := json.Marshal(r) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", sha1.Sum([]byte(repoJSON))), nil +} + +// GetUserEmail returns the email address that the user has used to configure git. +func (r *mockRepoForTest) GetUserEmail() (string, error) { return "user@example.com", nil } + +// GetCoreEditor returns the name of the editor that the user has used to configure git. +func (r *mockRepoForTest) GetCoreEditor() (string, error) { return "vi", nil } + +// PushRefs push git refs to a remote +func (r *mockRepoForTest) PushRefs(remote string, refPattern string) error { + return nil +}
\ No newline at end of file diff --git a/repository/repo.go b/repository/repo.go new file mode 100644 index 00000000..7329f183 --- /dev/null +++ b/repository/repo.go @@ -0,0 +1,20 @@ +// Package repository contains helper methods for working with a Git repo. +package repository + +// Repo represents a source code repository. +type Repo interface { + // GetPath returns the path to the repo. + GetPath() string + + // GetUserEmail returns the email address that the user has used to configure git. + GetUserEmail() (string, error) + + // GetCoreEditor returns the name of the editor that the user has used to configure git. + GetCoreEditor() (string, error) + + // PullRefs pull git refs from a remote + PullRefs(remote string, refPattern string) error + + // PushRefs push git refs to a remote + PushRefs(remote string, refPattern string) error +} |