diff --git a/lib/assisted_workflow/addons/git.rb b/lib/assisted_workflow/addons/git.rb index 6e9f505..b7545c6 100644 --- a/lib/assisted_workflow/addons/git.rb +++ b/lib/assisted_workflow/addons/git.rb @@ -2,18 +2,18 @@ require "assisted_workflow/addons/base" module AssistedWorkflow::Addons - + class GitError < AssistedWorkflow::Error; end - + class Git < Base - + DESCRIPTION_LIMIT = 30 - + def initialize(output, options = {}) super @command_options = {:raise_error => true}.merge(options) end - + # creates a new git branch based on story attributes # the branch name format is: # => story_onwer_username.story_id.story_name @@ -23,7 +23,7 @@ def create_story_branch(story, username) git "checkout -b #{branch}" # git "push --set-upstream origin #{branch}" end - + # run all the git steps required for a clean pull request def rebase_and_push log "preparing local branch" @@ -35,24 +35,24 @@ def rebase_and_push git "rebase master" git "push -u -f origin #{branch}" end - + # returns the current story id based on branch name def current_story_id current_branch.split(".")[1] end - + # returns the current local branch name def current_branch git("rev-parse --abbrev-ref HEAD", :silent => true) end - + # returns the repository name assigned to origin following the format: # owner/project def repository url = git("config --get remote.origin.url", :error => "cannot find 'origin' remote repository url") url.gsub("git@github.com:", "").gsub("https://github.com/", "").gsub(/\.git$/, "").chomp end - + # check if current branch is merged into master def check_merged! check_everything_commited! @@ -66,7 +66,7 @@ def check_merged! end merged end - + # removes current branch and its remote version def remove_branch log "removing local and remote feature branches" @@ -75,9 +75,16 @@ def remove_branch git "checkout master" git "branch -D #{branch}" end - + + def current_feature_name + branch = current_branch + branch = branch[branch.index(".") + 1, branch.length].gsub(/[\W_]/, " ") + branch[0] = branch[0].upcase + branch + end + private #================================================================= - + def git(command, options = {}) options = @command_options.merge(options) puts "git #{command}" unless options[:silent] == true @@ -88,24 +95,24 @@ def git(command, options = {}) end result end - + def system(command) %x{#{command}}.chomp end - + def system_error? $? != 0 end - + def branch_name(story, username) description = story.name.to_s.downcase.gsub(/\W/, "_").slice(0, DESCRIPTION_LIMIT) [username, story.id, description].join(".").downcase end - + def not_commited_changes git("status --porcelain", :silent => true).split("\n") end - + def check_everything_commited! raise AssistedWorkflow::Error, "git: there are not commited changes" unless not_commited_changes.empty? end diff --git a/lib/assisted_workflow/addons/github.rb b/lib/assisted_workflow/addons/github.rb index 7396a5f..5e6cfea 100644 --- a/lib/assisted_workflow/addons/github.rb +++ b/lib/assisted_workflow/addons/github.rb @@ -66,7 +66,7 @@ def create_pull_request(branch, story) pull_request = if story.is_a? GithubStory @client.create_pull_request_for_issue(@repo, base, branch, story.id) else - title = "[##{story.id}] #{story.name}" + title = story.id ? "[##{story.id}] #{story.name}" : story.name @client.create_pull_request(@repo, base, branch, title, story.description) end diff --git a/lib/assisted_workflow/cli.rb b/lib/assisted_workflow/cli.rb index 8d22ca9..f8c54dc 100644 --- a/lib/assisted_workflow/cli.rb +++ b/lib/assisted_workflow/cli.rb @@ -7,13 +7,13 @@ class CLI < Thor GLOBAL_CONFIG = File.expand_path(".awconfig", ENV["HOME"]) LOCAL_CONFIG = ".awconfig" source_root(File.expand_path(File.join(__FILE__, "..", "templates"))) - + # tasks shortcuts map ["-v", "--version"] => :version map "s" => :start map "u" => :submit map "f" => :finish - + desc "setup", "Setup initial configuration in current project directory" def setup copy_file "awconfig.global.tt", GLOBAL_CONFIG @@ -32,7 +32,7 @@ def setup c << "$ aw config pivotal.project_id=00001" end end - + desc "start [STORY_ID]", "Start the pivotal story and create a new branch to receive the changes" method_option :all, :type => :boolean, :default => false, :aliases => "-a", :desc => "Show started and pending stories when no story_id is provided" method_option :estimate, :type => :numeric, :aliases => "-e", :desc => "Sets the story estimate when starting" @@ -50,20 +50,29 @@ def start(story_id=nil) out.next_command "after commiting your changes, submit a pull request using:", "$ aw submit" end end - + desc "submit", "Submits the current story creating a new pull request" + method_option :force, :type => :boolean, :default => false, :aliases => "-f", :desc => "Create the pull request regardless of the current having an associated task or not" def submit check_awfile! story_id = git.current_story_id + unless story = tracker.find_story(story_id) - raise AssistedWorkflow::Error, "story not found, make sure a feature branch in active" + unless options[:force] + raise AssistedWorkflow::Error, "story not found, make sure a feature branch is active or use --force to ignore it altogether" + end end + git.rebase_and_push - pr_url = github.create_pull_request(git.current_branch, story) - tracker.finish_story(story, :note => pr_url) + pr_story = story || OpenStruct.new(name: git.current_feature_name) + pr_url = github.create_pull_request(git.current_branch, pr_story) + + if story + tracker.finish_story(story, :note => pr_url) + end out.next_command "after pull request approval, remove the feature branch using:", "$ aw finish" end - + desc "finish", "Check if the changes are merged into master, removing the current feature branch" def finish check_awfile! @@ -74,12 +83,12 @@ def finish git.remove_branch out.next_command "well done! check your next stories using:", "$ aw start" end - + desc "version", "Display assisted_workflow gem version" def version say AssistedWorkflow::VERSION end - + desc "config group.key=value", "Set configuration keys in local config file" method_option :global, :type => :boolean, :aliases => "-g", :desc => "Set configuration key in global configuration file (for all projects)" def config(*args) @@ -89,36 +98,36 @@ def config(*args) config_file.parse(args).save! end end - + desc "thanks", "Aw, Thanks!", :hide => true def thanks out.say "you're welcome!", :on_magenta end - - + + no_tasks do def out @out ||= Output.new(self.shell) end - + def tracker @tracker ||= Addons.load_tracker(out, configuration) || github end - + def git @git ||= Addons::Git.new(out) end - + def github - @github ||= Addons::Github.new(out, + @github ||= Addons::Github.new(out, {"repository" => git.repository}.merge(configuration[:github]) ) end - + def config_file @config_file ||= ConfigFile.new(awfile) end - + # loads all configuration, merging global and local values def configuration @configuration ||= begin @@ -128,7 +137,7 @@ def configuration end end end - + class << self def start(given_args=ARGV, config={}) super @@ -137,9 +146,9 @@ def start(given_args=ARGV, config={}) exit(1) end end - + private ################################################################## - + def check_awfile! raise AssistedWorkflow::Error, "#{awfile} does not exist.\nmake sure you run `$ aw setup` in your project folder." unless File.exist?(awfile) end diff --git a/spec/assisted_workflow/addons/git_spec.rb b/spec/assisted_workflow/addons/git_spec.rb index 8f7a1f5..0bb56d5 100644 --- a/spec/assisted_workflow/addons/git_spec.rb +++ b/spec/assisted_workflow/addons/git_spec.rb @@ -7,19 +7,19 @@ stub(@git).system_error?{ false } stub(@git).system("git rev-parse --abbrev-ref HEAD"){ "flavio.1234.new_feature"} end - + it "creates a story branch" do mock(@git).system("git checkout -b flavio.1234.new_feature") @git.create_story_branch(story, "flavio") end - + it "raises a git error when git command does not exit with success" do mock(@git).system_error?{ true } mock(@git).system("git checkout -b flavio.1234.new_feature") proc { @git.create_story_branch(story, "flavio") }.must_raise AssistedWorkflow::Addons::GitError, "git command error" end - - + + it "rebases and push a feature branch" do mock(@git).system("git status --porcelain"){ "" } mock(@git).system("git checkout master") @@ -29,57 +29,62 @@ mock(@git).system("git push -u -f origin flavio.1234.new_feature") @git.rebase_and_push end - + it "raises when rebasing if there are not commited changes" do mock(@git).system("git status --porcelain"){ "changed_file.rb" } - proc { + proc { @git.rebase_and_push }.must_raise AssistedWorkflow::Error, "git: there are not commited changes" end - + it "returns the story_id from branch name" do @git.current_story_id.must_equal "1234" end - + it "return the current branch name" do @git.current_branch.must_equal "flavio.1234.new_feature" end - + it "returns the repository name assigned to origin" do mock(@git).system("git config --get remote.origin.url"){ "git@github.com:flaviogranero/assisted_workflow.git"} @git.repository.must_equal "flaviogranero/assisted_workflow" end - + + it "returns the feature name" do + stub(@git).system("git rev-parse --abbrev-ref HEAD"){ "marcioj.some-amazing_feature.that.i-did" } + @git.current_feature_name.must_equal "Some amazing feature that i did" + end + describe "#check_merged!" do - + before do mock(@git).system("git status --porcelain"){ "" } mock(@git).system("git checkout flavio.1234.new_feature") mock(@git).system("git checkout master") mock(@git).system("git pull --rebase") end - + it "returns true if current branch is merged into master" do mock(@git).system("git branch --merged"){ "flavio.1234.new_feature" } @git.check_merged!.must_equal true end - + it "returns false if current branch is not merged into master" do mock(@git).system("git branch --merged"){ "flavio.1234.other_feature" } proc { @git.check_merged! }.must_raise AssistedWorkflow::Error, "this branch is not merged into master" - + end end - + it "removes current branch and its remote version" do mock(@git).system("git push origin :flavio.1234.new_feature") mock(@git).system("git checkout master") mock(@git).system("git branch -D flavio.1234.new_feature") @git.remove_branch end - + private #================================================================== - + def story # stubs @client = TrackerApi::Client.new(token: "mypivotaltoken") diff --git a/spec/assisted_workflow/addons/github_spec.rb b/spec/assisted_workflow/addons/github_spec.rb index 8dcff6a..545c6b1 100644 --- a/spec/assisted_workflow/addons/github_spec.rb +++ b/spec/assisted_workflow/addons/github_spec.rb @@ -9,27 +9,27 @@ } @client = client_stub stub(Octokit::Client).new{ @client } - + @github = AssistedWorkflow::Addons::Github.new(nil, @configuration) end - + it "initializes a valid github wrapper" do assert @github.valid? end - + it "requires token and repository configuration" do - proc { + proc { AssistedWorkflow::Addons::Github.new(nil, {}) }.must_raise AssistedWorkflow::Error, "github missing configuration:[token,repository]" end - + it "creates a new valid pull request from a pivotal story" do mock(@client).create_pull_request("fakeuser/fakerepo", "master", "fakeuser.1234.new_feature", "[#1234] New Feature", "Feature description"){ pull_request } @github.create_pull_request( "fakeuser.1234.new_feature", story ).must_match /fakeuser\/fakerepo\/pull\/1/ end - + it "creates a new valid pull request from a github story" do mock(@client).create_pull_request_for_issue("fakeuser/fakerepo", "master", "fakeuser.1234.new_feature", 10){ pull_request } @github.create_pull_request( @@ -37,48 +37,55 @@ AssistedWorkflow::Addons::GithubStory.new(gh_issue(:number => 10)) ).must_match /fakeuser\/fakerepo\/pull\/1/ end - + + it "creates a new valid pull request from a story without id" do + mock(@client).create_pull_request("fakeuser/fakerepo", "master", "fakeuser.1234.new_feature", "My awesome feature", nil){ pull_request } + @github.create_pull_request( + "fakeuser.1234.new_feature", OpenStruct.new(name: "My awesome feature") + ).must_match /fakeuser\/fakerepo\/pull\/1/ + end + it "raises on creating an invalid pull request" do mock(@client).create_pull_request("fakeuser/fakerepo", "master", "fakeuser.1234.new_feature", "[#1234] New Feature", "Feature description"){ nil } - proc { + proc { @github.create_pull_request( "fakeuser.1234.new_feature", story ) }.must_raise AssistedWorkflow::Error, "error on submiting the pull request" end - + it "finds a story by id" do mock(@client).issue(@configuration["repository"], "10") do |repo, issue_number| gh_issue(:number => issue_number) end - + story = @github.find_story("10") story.id.must_equal "10" end - + it "returns pending stories" do mock(@client).issues(@configuration["repository"], { :state => "open", :assignee => "fakeuser" }) do [ gh_issue ] end - + stories = @github.pending_stories(:include_started => false) stories.size.must_equal 1 end - + it "starts a story" do mock(@client).reopen_issue(@configuration["repository"], gh_issue.number, :assignee => "fakeuser", :labels => ["bug","started"]){ true } @github.start_story(AssistedWorkflow::Addons::GithubStory.new(gh_issue)) end - + it "finishes a story" do mock(@client).reopen_issue(@configuration["repository"], gh_issue.number, :assignee => "fakeuser", :labels => ["bug","finished"]){ true } @github.finish_story(AssistedWorkflow::Addons::GithubStory.new(gh_issue)) end - + private #================================================================== - + def story # stubs @client = TrackerApi::Client.new(token: "mypivotaltoken") @@ -94,22 +101,22 @@ def story @story end - + def agent_stub Sawyer::Agent.new("", {:links_parser => Sawyer::LinkParsers::Simple.new}) end - + def gh_issue(attributes = {}) @gh_issue ||= Sawyer::Resource.new(agent_stub, attributes.merge({ :assignee => {:login => "fakeuser"}, :labels => [{:name => "bug"}] })) end - + def pull_request Sawyer::Resource.new(agent_stub, {_links: {html: {href: "https://github.com/fakeuser/fakerepo/pull/1"}}}) end - + def client_stub client = Object.new user = Object.new