diff --git a/README.md b/README.md index 0227c413..06286393 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ This design removes the need for any sort of server-side setup. As a result, this tool can work with any git hosting provider, and the only setup required is installing the client on your workstation. +Additionally, code reviews can be conducted across multiple hosting providers. +The list of forks of a repository is also stored in the repository as git +objects, allowing code reviews to be pulled from every registered fork. + ## Installation Assuming you have the [Go tools installed](https://golang.org/doc/install), run @@ -80,8 +84,15 @@ Submitting the current review: git appraise submit [--merge | --rebase] +Adding a fork: + + git appraise fork add -o [,]* + A more detailed getting started doc is available [here](docs/tutorial.md). +Instructions for using `git-appraise` with multiple forks can be found +[here](docs/forks.md). + ## Metadata The code review data is stored in [git-notes](https://git-scm.com/docs/git-notes), diff --git a/commands/commands.go b/commands/commands.go index 75b8c72d..b4ea0390 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -18,12 +18,17 @@ limitations under the License. package commands import ( + "fmt" + "github.com/google/git-appraise/repository" ) -const notesRefPattern = "refs/notes/devtools/*" -const archiveRefPattern = "refs/devtools/archives/*" -const commentFilename = "APPRAISE_COMMENT_EDITMSG" +const ( + notesRefPattern = "refs/notes/devtools/*" + devtoolsRefPattern = "refs/devtools/*" + archiveRefPattern = "refs/devtools/archives/*" + commentFilename = "APPRAISE_COMMENT_EDITMSG" +) // Command represents the definition of a single command. type Command struct { @@ -41,15 +46,41 @@ func (cmd *Command) Run(repo repository.Repo, args []string) error { // CommandMap defines all of the available (sub)commands. var CommandMap = map[string]*Command{ - "abandon": abandonCmd, - "accept": acceptCmd, - "comment": commentCmd, - "list": listCmd, - "pull": pullCmd, - "push": pushCmd, - "rebase": rebaseCmd, - "reject": rejectCmd, - "request": requestCmd, - "show": showCmd, - "submit": submitCmd, + "abandon": abandonCmd, + "accept": acceptCmd, + "comment": commentCmd, + "fork add": addForkCmd, + "fork list": listForksCmd, + "fork remove": removeForkCmd, + "list": listCmd, + "pull": pullCmd, + "push": pushCmd, + "rebase": rebaseCmd, + "reject": rejectCmd, + "request": requestCmd, + "show": showCmd, + "submit": submitCmd, +} + +// FindSubcommand parses the subcommand from the list of arguments. +// +// The args parameter is the list of command line args after the program name. +// +// The return result are the matching command (if found), whether or not the +// command was found, and the list of remaining command line arguments that +// followed the subcommand. +func FindSubcommand(args []string) (*Command, bool, []string) { + if len(args) < 1 { + subcommand, ok := CommandMap["list"] + return subcommand, ok, []string{} + } + subcommand, ok := CommandMap[args[0]] + if ok { + return subcommand, ok, args[1:] + } + if len(args) > 1 { + subcommand, ok := CommandMap[fmt.Sprintf("%s %s", args[0], args[1])] + return subcommand, ok, args[2:] + } + return nil, false, []string{} } diff --git a/commands/commands_test.go b/commands/commands_test.go new file mode 100644 index 00000000..7bb527da --- /dev/null +++ b/commands/commands_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2018 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "strings" + "testing" +) + +func TestFindSubcommandBuiltins(t *testing.T) { + for name, cmd := range CommandMap { + additionalArg := "foo" + matchingArgs := append(strings.Split(name, " "), additionalArg) + subcommand, ok, remainingArgs := FindSubcommand(matchingArgs) + if !ok { + t.Errorf("Failed to find the built-in subcommand %q", name) + } else if subcommand != cmd { + t.Errorf("Return the wrong subcommand for %q", name) + } else if len(remainingArgs) != 1 || remainingArgs[0] != additionalArg { + t.Errorf("Failed to return the remaining arguments for %q", name) + } + } +} + +func TestFindSubcommandEmpty(t *testing.T) { + subcommand, ok, remaining := FindSubcommand([]string{}) + if !ok { + t.Fatalf("Failed to return a default subcommand") + } + if subcommand != CommandMap["list"] { + t.Fatalf("Failed to return `list` as the default subcommand") + } + if len(remaining) != 0 { + t.Fatalf("Unexpected remaining arguments for an empty command: %q", remaining) + } +} diff --git a/commands/fork.go b/commands/fork.go new file mode 100644 index 00000000..7eac438c --- /dev/null +++ b/commands/fork.go @@ -0,0 +1,111 @@ +/* +Copyright 2018 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package commands + +import ( + "errors" + "flag" + "fmt" + "strings" + + "github.com/google/git-appraise/commands/output" + "github.com/google/git-appraise/fork" + "github.com/google/git-appraise/repository" +) + +var ( + addForkFlagSet = flag.NewFlagSet("fork add", flag.ExitOnError) + addForkOwners = addForkFlagSet.String("o", "", "Comma-separated list of owner email addresses") +) + +// addFork updates the local git repository to include the specified fork. +func addFork(repo repository.Repo, args []string) error { + addForkFlagSet.Parse(args) + args = addForkFlagSet.Args() + + var owners []string + if len(*addForkOwners) > 0 { + for _, owner := range strings.Split(*addForkOwners, ",") { + owners = append(owners, strings.TrimSpace(owner)) + } + } + if len(args) < 2 { + return errors.New("The name and URL of the fork must be specified.") + } + if len(args) > 2 { + return errors.New("Only the name and URL of the fork may be specified.") + } + if len(owners) == 0 { + return errors.New("You must specify at least one owner.") + } + name := args[0] + url := args[1] + return fork.Add(repo, fork.New(name, url, owners)) +} + +// listForks lists the forks registered in the local git repository. +func listForks(repo repository.Repo, args []string) error { + forks, err := fork.List(repo) + if err != nil { + return err + } + output.PrintForks(forks) + return nil +} + +// removeFork updates the local git repository to no longer include the specified fork. +func removeFork(repo repository.Repo, args []string) error { + if len(args) < 1 { + return errors.New("The name of the fork must be specified.") + } + if len(args) > 1 { + return errors.New("Only the name of the fork may be specified.") + } + name := args[0] + return fork.Delete(repo, name) +} + +// addForkCmd defines the `fork add` command. +var addForkCmd = &Command{ + Usage: func(arg0 string) { + fmt.Printf("Usage: %s fork add [