pax_global_header00006660000000000000000000000064127164273620014524gustar00rootroot0000000000000052 comment=33eef716cf6f0b2e0d4ec4cecda94332b91168d0 git-reintegrate-0.4/000077500000000000000000000000001271642736200144615ustar00rootroot00000000000000git-reintegrate-0.4/.travis.yml000066400000000000000000000001241271642736200165670ustar00rootroot00000000000000language: ruby script: - make test rvm: - 2.2.0 - 2.1.0 - 2.0.0 - 1.9.3 git-reintegrate-0.4/Makefile000066400000000000000000000010371271642736200161220ustar00rootroot00000000000000prefix := $(HOME) bindir := $(prefix)/bin mandir := $(prefix)/share/man/man1 all: doc doc: doc/git-reintegrate.1 test: $(MAKE) -C test doc/git-reintegrate.1: doc/git-reintegrate.txt a2x -d manpage -f manpage $< clean: $(RM) doc/git-reintegrate.1 D = $(DESTDIR) install: install -d -m 755 $(D)$(bindir)/ install -m 755 git-reintegrate $(D)$(bindir)/git-reintegrate install-doc: doc install -d -m 755 $(D)$(mandir)/ install -m 644 doc/git-reintegrate.1 $(D)$(mandir)/git-reintegrate.1 .PHONY: all test install install-doc clean git-reintegrate-0.4/README.asciidoc000066400000000000000000000033531271642736200171220ustar00rootroot00000000000000= git-reintegrate = This tool helps to manage integration branches. For example, say you have a repository with three branches: * feature-a * feature-b * maint And you have an integration branch named 'integration' where you merge all these branches on top of 'master'. You can generate the instructions needed by `git reintegrate` with this command: ------------ git reintegrate --generate integration master ------------ Which would generate instructions like: ------------ base master merge feature-a Merge work in progress feature-a merge feature-b Merge feature-b merge maint Merge good stuff ------------ You can edit the instructions with `git reintegrate --edit`. The simplest way to begin an integration branch is with: ------------ git reintegrate --create integration master git reintegrate --add=branch1 --add=branch2 --add=branch3 ------------ To regenerate the integration branch run `git reintegrate --rebuild`, if there are merge conflicts, solve them and continue with `git reintegrate --continue`. You probably want to configure `git rerere` so that each time you resolve a conflict it gets automatically stored, so the next time Git sees the conflict, it's resolved automatically: ------------ git config --global rerere.enabled true ------------ == Installation == Simply copy the script anywhere in your '$PATH' and make it executable, or run `make install` which will install it by default to your '~/bin/' directory (make sure it's in your '$PATH'). == Acknowledgements == This is a rewrite of John Keeping's `git integration` tool (https://github.com/johnkeeping/git-integration[link]) , that provides a one-to-one mapping of functionality, plus some extras. Also, it borrows ideas from git.git's integration scripts. git-reintegrate-0.4/doc/000077500000000000000000000000001271642736200152265ustar00rootroot00000000000000git-reintegrate-0.4/doc/.gitignore000066400000000000000000000000221271642736200172100ustar00rootroot00000000000000git-reintegrate.1 git-reintegrate-0.4/doc/git-reintegrate.txt000066400000000000000000000102761271642736200210670ustar00rootroot00000000000000git-reintegrate(1) ================== NAME ---- git-reintegrate - Manage integration branches in Git SYNOPSIS -------- [verse] 'git reintegrate' --create [] 'git-reintegrate' --generate [] 'git-reintegrate' --add= 'git-reintegrate' (--edit | --rebuild | --apply | --cat | --status) [] 'git reintegrate' (--continue | --abort) 'git-reintegrate' --list DESCRIPTION ----------- This tool is a helper to be able to manage integration branches in Git easily. It does so by specifying a list of merges to be applied on top of a base branch. Each one of these merges can have a description that will be used as the merge commit message. This instruction sheet can be autogenerated and modified in various ways through the command line, or manually edited. Finally the integration branch can be rebuilt, and previous conflicts resolutions can be reused thanks to `git rerere`. OPTIONS ------- --create:: Create a new integration branch. + If no base is specified, 'master' is assumed. --generate:: Generates the instruction sheet based on an existing integration branch. The messages of each merge commit will be parsed to construct the instructions. The messages should be in the standard form `Merge branch 'name'` (or remote branch). + If no base is specified, 'master' is assumed. --[no-]rebuild:: Rebuild the integration branch. --edit:: Edit the instruction sheet. --cat:: Print the instruction sheet. --status:: Prints the status of the integration branch. --add=:: Appends a line `merge ` to the instruction list, causing that branch to be included in the integration branch when it is next rebuilt. This option may be specified multiple times to add multiple branches. --continue:: Restart the rebuild process after having resolved a merge conflict. --abort:: Abort the rebuild operation. --[no-]autocontinue:: Continues automatically if the rerere mechanism manages to resolve all conflicts during a merge. --list:: List all the current integration branches. --delete:: Delete an integration branch. It doesn't delete the branch itself, only the integration information. --apply:: Applies the instruction sheet of the integration branch on top of the current branch. If a branch to merge is already completely merged in the current branch, it's skipped. If `--continue` or `--abort` are specified then no other options may be given. CONFIGURATION ------------- integration.autocontinue:: Sets the default for the `--autocontinue` option. integration.autorebuild:: Automatically rebuild the integration branch after creating/editing it if `--no-rebuild` is not specified. FORMAT OF INSTRUCTIONS ---------------------- The instruction sheet consists of a series of instructions which begin in column zero, each of which may be followed by indented comments. The following instructions are supported: +base+ '':: Resets the state of the integration branch to the specified revision. This should always be the first instruction in the instruction sheet, and should appear only at the top of the instruction sheet. +merge+ '' '[]':: Merges the specified ref into the integration branch. Any comments following the instruction are appended to the merge commit's commit message. + If any options are given after the ref (and on the same line) then these are passed to 'git merge'. This may be useful for specifying an alternative merge strategy for a branch. +fixup+ '':: Picks up an existing commit to be applied on top of a merge as a fixup. +commit+:: Adds an empty commit with the specified message mostly to be used as a comment. +pause+:: Pauses the rebuild process, so the user can do certain actions manually, for example fixing a wrong conflict resolution. +.+ '':: Ignores this command and its description. This can be used to remove a branch from the integration branch without needing to delete it and its description from the instruction sheet. Example ~~~~~~~ ------ base master merge my-experimental-feature I think this is a good idea, but want to dogfood it before I decide whether to submit it upstream. merge my-site-specific-changes Some changes to suit my environment. DO NOT SUBMIT THESE. ------ git-reintegrate-0.4/git-reintegrate000077500000000000000000000450531271642736200175100ustar00rootroot00000000000000#!/usr/bin/env ruby # # Copyright (C) 2013-2014 Felipe Contreras # # This file may be used under the terms of the GNU GPL version 2. # require 'fileutils' $merged = [] $actions = [] $need_rebuild = false $branches_to_add = [] $autocontinue = false NULL_SHA1 = '0' * 40 def die(*args) fmt = args.shift $stderr.printf("fatal: %s\n" % fmt, *args) exit 128 end def git_editor(*args) editor = %x[git var GIT_EDITOR].chomp.split(' ') system(*editor, *args) end class ParseOpt attr_writer :usage class Option attr_reader :short, :long, :help def initialize(short, long, help, &block) @block = block @short = short @long = long @help = help end def call(v) @block.call(v) end end def initialize @list = {} end def on(short = nil, long = nil, help = nil, &block) opt = Option.new(short, long, help, &block) @list[short] = opt if short @list[long] = opt if long end def parse if ARGV.member?('-h') or ARGV.member?('--help') usage exit 0 end seen_dash = false ARGV.delete_if do |cur| opt = val = nil next false if cur[0,1] != '-' or seen_dash case cur when '--' seen_dash = true next true when /^--no-(.+)$/ opt = @list[$1] val = false when /^-([^-])(.+)?$/, /^--(.+?)(?:=(.+))?$/ opt = @list[$1] val = $2 || true end if opt opt.call(val) true else usage exit 1 end end end def usage def fmt(prefix, str) return str ? prefix + str : nil end puts 'usage: %s' % @usage @list.values.uniq.each do |opt| s = ' ' s << '' s << [fmt('-', opt.short), fmt('--', opt.long)].compact.join(', ') s << '' s << '%*s%s' % [26 - s.size, '', opt.help] if opt.help puts s end end end def parse_merge(other, commit, msg, body) case msg when /^Merge branch '(.*)'/ ref = 'refs/heads/' + $1 when /^Merge remote branch '(.*)'/ ref = 'refs/' + $1 else $stderr.puts "Huh?: #{msg}" return end merged = %x[git name-rev --refs="#{ref}" "#{other}" 2> /dev/null].chomp if merged =~ /\h{40} (.*)/ merged = $1 end merged = "merge #{merged}" merged += "\n\n" + body.gsub(/^/, ' ') unless body.empty? merged end class Branch attr_reader :name, :ref, :int attr_reader :into_name, :into_ref def initialize(name) @name = name end def into_name=(name) @into_name = name @into_ref = %x[git rev-parse --symbolic-full-name "refs/heads/#{name}"].chomp end def get if @name @ref = %x[git rev-parse --symbolic-full-name "refs/heads/#{@name}"].chomp die "no such branch: #{@name}" unless $?.success? else @ref = %x[git symbolic-ref HEAD].chomp die "HEAD is detached, could not figure out which integration branch to use" unless $?.success? @name = @ref.gsub(%r{^refs/heads/}, '') end @int = @ref.gsub(%r{^refs/heads/}, 'refs/int/') system(*%w[git rev-parse --quiet --verify], @int, :out => File::NULL) die "Not an integration branch: #{@name}" unless $?.success? end def create(base = nil) @ref = %x[git check-ref-format --normalize "refs/heads/#{@name}"].chomp die "invalid branch name: #{@name}" unless $?.success? if base system(*%w[git rev-parse --quiet --verify], "#{base}^{commit}", :out => File::NULL) die "no such commit: #{base}" unless $?.success? else base = 'master' end @int = @ref.gsub(%r{^refs/heads/}, 'refs/int/') system(*%w[git update-ref], @ref, base, NULL_SHA1) write_instructions("base #{base}\n") system(*%w[git checkout], @name) puts "Integration branch #{@name} created." end def generate(base = nil) if @name @ref = %x[git rev-parse --symbolic-full-name "refs/heads/#{@name}"].chomp die "no such branch: #{@name}" unless $?.success? else @ref = %x[git symbolic-ref HEAD].chomp die "HEAD is detached, could not figure out which integration branch to use" unless $?.success? @name = @ref.gsub(%r{^refs/heads/}, '') end if base system(*%w[git rev-parse --quiet --verify], "#{base}^{commit}", :out => File::NULL) die "no such commit: #{base}" unless $?.success? else base = 'master' end @int = @ref.gsub(%r{^refs/heads/}, 'refs/int/') series = [] series << "base #{base}" IO.popen(%w[git log --no-decorate --format=%H%n%B -z --reverse --first-parent] + ["^#{base}", @name]) do |io| io.each("\0") do |l| commit, summary, body = l.chomp("\0").split("\n", 3) body.lstrip! other = %x[git rev-parse -q --verify "#{commit}^2"].chomp if not other.empty? body.gsub!(/\n?(\* .*:.*\n.*)/m, '') series << parse_merge(other, commit, summary, body) end end end write_instructions(series.join("\n") + "\n") puts "Integration branch #{@name} generated." end def read_instructions %x[git cat-file blob #{@int}:instructions].chomp end def write_instructions(content) insn_blob = insn_tree = insn_commit = nil parent = %x[git rev-parse --quiet --verify #{@int}].chomp parent_tree = %x[git rev-parse --quiet --verify #{@int}^{tree}].chomp parent = nil if parent.empty? IO.popen(%[git hash-object -w --stdin], 'r+') do |io| io.write(content) io.close_write insn_blob = io.read.chomp end die "Failed to write instruction sheet blob object" unless $?.success? IO.popen(%[git mktree], 'r+') do |io| io.printf "100644 blob %s\t%s\n", insn_blob, 'instructions' io.close_write insn_tree = io.read.chomp end die "Failed to write instruction sheet tree object" unless $?.success? # If there isn't anything to commit, stop now. return if insn_tree == parent_tree op = parent ? 'Update' : 'Create' opts = parent ? ['-p', parent] : [] opts << insn_tree IO.popen(%w[git commit-tree] + opts, 'r+') do |io| io.write("#{op} integration branch #{@int}") io.close_write insn_commit = io.read.chomp end die "Failed to write instruction sheet commit" unless $?.success? system(*%w[git update-ref], @int, insn_commit, parent || NULL_SHA1) die "Failed to update instruction sheet reference" unless $?.success? end end class Integration attr_reader :commands class Stop < Exception end class Pause < Exception end @@map = { '.' => :cmd_dot } def initialize(obj) self.load(obj) end def load(obj) cmd, args = nil msg = "" cmds = [] obj.each_line do |l| l.chomp! case l when '' when /^\s(.*)$/ msg << $1 when /(\S+) ?(.*)$/ cmds << [cmd, args, msg] if cmd cmd, args = [$1, $2] msg = "" end end cmds << [cmd, args, msg] if cmd @commands = cmds end def self.run(obj) self.new(obj).run end def self.start(branch, into_name, inst) require_clean_work_tree('integrate', "Please commit or stash them.") orig_head = %x[git rev-parse --quiet --verify "#{branch}^{commit}"].chomp system(*%w[git update-ref ORIG_HEAD], orig_head) system(*%w[git checkout --quiet], "#{branch}^0") die "could not detach HEAD" unless $?.success? FileUtils.mkdir_p($state_dir) File.write($head_file, branch) commit = %x[git rev-parse --quiet --verify #{branch}].chomp File.write($start_file, commit) $branch.into_name = into_name File.write($into_file, into_name) File.write($insns, inst) self.run(inst) end def run begin while cmd = @commands.first finalize_command(*cmd) @commands.shift end rescue Integration::Stop => e stop(e.message) rescue Integration::Pause => e @commands.shift stop(e.message) else finish end end def finalize_command(cmd, args, message) begin fun = @@map[cmd] || "cmd_#{cmd}".to_sym send(fun, message, *args) rescue NoMethodError raise Integration::Stop, "Unknown command: #{cmd}" end end def finish system(*%w[git update-ref], $branch.into_ref, 'HEAD', File.read($start_file)) system(*%w[git symbolic-ref], 'HEAD', $branch.into_ref) FileUtils.rm_rf($state_dir) system(*%w[git gc --auto]) if $branch.name == $branch.into_name puts "Successfully re-integrated #{$branch.name}." else puts "Successfully applied #{$branch.name} into #{$branch.into_name}." end end def stop(msg = nil) File.open($insns, 'w') do |f| @commands.each do |cmd, args, msg| str = "%s %s\n" % [cmd, args] str += "%s\n" % msg if msg and not msg.empty? f.write(str) end end File.write($merged_file, $merged.join("\n")) $stderr.puts(msg) if msg and ! msg.empty? $stderr.puts < ':' }, *%w[git commit --amend -a]) raise Integration::Stop, '' unless $?.success? end def cmd_commit(message, *args) puts "Emtpy commit" system(*%w[git commit --allow-empty -m], message) raise Integration::Stop, '' unless $?.success? end def cmd_pause(message, *args) raise Integration::Pause, (message || 'Pause') end def cmd_dot(message, *args) end def require_clean_work_tree(action = nil, msg = nil, quiet = false) system(*%w[git update-index -q --ignore-submodules --refresh]) errors = [] system(*%w[git diff-files --quiet --ignore-submodules]) errors << "Cannot #{action}: You have unstaged changes." unless $?.success? system(*%w[git diff-index --cached --quiet --ignore-submodules HEAD --]) if not $?.success? if errors.empty? errors << "Cannot #{action}: Your index contains uncommitted changes." else errors << "Additionally, your index contains uncommitted changes." end end if not errors.empty? and not quiet errors.each do |e| $stderr.puts(e) end $stderr.puts(msg) if msg exit 1 end return errors.empty? end def do_rebuild inst = $branch.read_instructions die "Failed to read instruction list for branch #{$branch.name}" unless $?.success? Integration.start($branch.name, $branch.name, inst) end def get_head_file die "no integration in progress" unless test('f', $head_file) branch_name = File.read($head_file).gsub(%r{^refs/heads/}, '') branch = Branch.new(branch_name) branch.get branch.into_name = File.read($into_file) return branch end def do_continue $branch = get_head_file if File.exists?("#{$git_dir}/MERGE_HEAD") # We are being called to continue an existing operation, # without the user having manually committed the result of # resolving conflicts. system(*%w[git update-index --ignore-submodules --refresh]) && system(*%w[git diff-files --quiet --ignore-submodules]) || die("You must edit all merge conflicts and then mark them as resolved using git add") system(*%w[git commit --quiet --no-edit]) die "merge_head" unless $?.success? end $merged = File.read($merged_file).split("\n") File.open($insns) do |f| Integration.run(f) end end def do_abort $branch = get_head_file system(*%w[git symbolic-ref HEAD], $branch.into_ref) && system(*%w[git reset --hard], $branch.into_ref) && FileUtils.rm_rf($state_dir) end def do_apply head = %x[git rev-parse --symbolic --abbrev-ref HEAD].chomp inst = $branch.read_instructions die "Failed to read instruction list for branch #{$branch.name}" unless $?.success? inst = inst.lines.reject do |line| next true if line =~ /^base / if line =~ /^merge (.*)$/ system(*%W[git merge-base --is-ancestor #{$1} HEAD]) next true if $?.success? end false end.join Integration.start(head, head, inst) end def status_merge(branch_to_merge = nil) if not branch_to_merge $stderr.puts "no branch specified with 'merge' command" return end $status_base ||= 'master' if ! system(*%w[git rev-parse --verify --quiet], "#{branch_to_merge}^{commit}", :out => File::NULL) state = "." verbose_state = "branch not found" elsif system(*%w[git merge-base --is-ancestor], branch_to_merge, $status_base) state = "+" verbose_state = "merged to #{$status_base}" elsif system(*%w[git-merge-base --is-ancestor], branch_to_merge, $branch.name) state = "*" verbose_state = "up-to-date" else state = "-" verbose_state = "branch changed" end printf("%s %-*s(%s)", state, $longest_branch, branch_to_merge, verbose_state) puts $message.gsub(/^./, ' \&') if $message puts log = %x[git --no-pager log --oneline --cherry "#{$status_base}...#{branch_to_merge}" -- 2> /dev/null].chomp print(log.gsub(/^/, ' ') + "\n\n") unless log.empty? end def status_dot(*args) args.each do |arg| puts ". #{arg}\n" end puts $message.gsub(/^./, ' \&') if $message end def do_status inst = $branch.read_instructions die "Failed to read instruction list for branch #{$branch.name}" unless $?.success? int = Integration.new(inst) cmds = int.commands $longest_branch = cmds.map do |cmd, args, msg| next 0 if cmd != 'merge' args.split(' ').first.size end.max cmds.each do |cmd, args, msg| case cmd when 'base' $status_base = args when 'merge' status_merge(*args) when '.' status_dot(*args) else $stderr.puts "unhandled command: #{cmd} #{args}" end end end opts = ParseOpt.new opts.usage = 'git reintegrate' opts.on('c', 'create', 'create a new integration branch') do |v| $create = true $need_rebuild = true end opts.on('e', 'edit', 'edit the instruction sheet for a branch') do |v| $edit = true $need_rebuild = true end opts.on('r', 'rebuild', 'rebuild an integration branch') do |v| $rebuild = v end opts.on(nil, 'continue', 'continue an in-progress rebuild') do |v| $actions << :continue end opts.on(nil, 'abort', 'abort an in-progress rebuild') do |v| $actions << :abort end opts.on('g', 'generate', 'generate instruction sheet') do |v| $generate = true end opts.on('a', 'add', 'add a branch to merge in the instruction sheet') do |v| $branches_to_add << v $need_rebuild = true end opts.on(nil, 'autocontinue', 'continue automatically on merge conflicts if possible') do |v| $autocontinue = v end opts.on(nil, 'cat', 'show the instructions of an integration branch') do |v| $cat = v end opts.on('s', 'status', 'shows the status of all the dependencies of an integration branch') do |v| $status = true end opts.on('l', 'list', 'list integration branches') do |v| $list = true end opts.on('d', 'delete', 'delete an integration branch') do |v| $delete = true end opts.on(nil, 'apply', 'apply an integration branch on the current branch') do |v| $apply = true end %x[git config --bool --get integration.autocontinue].chomp == "true" && $autocontinue = true opts.parse $git_cdup = %x[git rev-parse --show-cdup].chomp die "You need to run this command from the toplevel of the working tree." unless $git_cdup.empty? $git_dir = %x[git rev-parse --git-dir].chomp $state_dir = "#{$git_dir}/integration" $start_file = "#{$state_dir}/start-point" $head_file = "#{$state_dir}/head-name" $merged_file = "#{$state_dir}/merged" $insns = "#{$state_dir}/instructions" $into_file = "#{$state_dir}/into" case $actions.first when :continue do_continue when :abort do_abort end if $list IO.popen(%w[git for-each-ref refs/int/]).each do |line| if line =~ %r{.*\trefs/int/(.*)} puts $1 end end exit end if $delete name = ARGV[0] msg = 'reintegrate: delete branch' system(*%W[git update-ref -d refs/int/#{name}] + ['-m', msg]) exit end $branches_to_add.each do |branch| system(*%w[git rev-parse --quiet --verify], "#{branch}^{commit}", :out => File::NULL) die "not a valid commit: #{branch}" unless $?.success? end $branch = Branch.new(ARGV[0]) if $create $branch.create(ARGV[1]) elsif $generate $branch.generate(ARGV[1]) else $branch.get end if $edit || ! $branches_to_add.empty? do_edit end if $cat puts $branch.read_instructions end if $rebuild == nil && $need_rebuild == true %x[git config --bool --get integration.autorebuild].chomp == "true" && $rebuild = true end if $rebuild do_rebuild end if $apply do_apply end if $status do_status end git-reintegrate-0.4/shared/000077500000000000000000000000001271642736200157275ustar00rootroot00000000000000git-reintegrate-0.4/shared/git-reintegrate.bash000066400000000000000000000006201271642736200216560ustar00rootroot00000000000000#!bash _git_reintegrate () { case "$cur" in --add=*) __gitcomp_nl "$(__git_refs)" "" "${cur##--add=}" return ;; -*) __gitcomp " --create --edit --rebuild --continue --abort --generate --cat --status --add= --prefix= --autocontinue" return ;; esac __gitcomp_nl "$(git --git-dir="$(__gitdir)" \ for-each-ref --format='%(refname)' refs/int | sed -e 's#^refs/int/##')" } git-reintegrate-0.4/test/000077500000000000000000000000001271642736200154405ustar00rootroot00000000000000git-reintegrate-0.4/test/.gitignore000066400000000000000000000000501271642736200174230ustar00rootroot00000000000000test-results/ trash directory.*/ .prove git-reintegrate-0.4/test/Makefile000066400000000000000000000002701271642736200170770ustar00rootroot00000000000000RM ?= rm -f T = $(wildcard *.t) all: test test: $(T) $(MAKE) clean $(T): $(SHELL) $@ $(TEST_OPTS) clean: $(RM) -r 'trash directory'.* test-results .PHONY: all test $(T) clean git-reintegrate-0.4/test/apply.t000077500000000000000000000025751271642736200167660ustar00rootroot00000000000000#!/bin/sh # # Copyright (C) 2014 Felipe Contreras # # This file may be used under the terms of the GNU GPL version 2. # test_description='Test git reintegrage apply option' . ./test-lib.sh test_expect_success 'setup branches' ' git init -q && commit_file base base && git checkout -b feature-1 master && commit_file feature-1 feature-1 && git checkout -b feature-2 master && commit_file feature-2 feature-2 && git checkout -b next master && git merge --no-ff feature-1 && git merge --no-ff feature-2 ' test_expect_success 'generate integration' ' git reintegrate --create integration && git reintegrate --add=feature-1 --add=feature-2 && git reintegrate --rebuild && > expected && git diff next integration > actual && test_cmp expected actual ' test_expect_success 'update integration' ' git checkout feature-1 && commit_file feature-1-fix feature-1-fix && git reintegrate --rebuild integration && git diff feature-1^..feature-1 > expected && git diff next integration > actual && test_cmp expected actual ' cat > expected < actual && test_cmp expected actual && > expected && git diff next integration > actual && test_cmp expected actual ' test_done git-reintegrate-0.4/test/conflicts.t000077500000000000000000000063371271642736200176250ustar00rootroot00000000000000#!/bin/sh # # Copyright (C) 2013-2014 Felipe Contreras # Copyright (C) 2013 John Keeping # # This file may be used under the terms of the GNU GPL version 2. # test_description='Test git reintegrage branches with no conflicts' . ./test-lib.sh test_expect_success 'setup branches' ' git init -q && commit_file base base && git checkout -b branch1 && commit_file base branch1 && git checkout -b branch2 master && commit_file base branch2 && git checkout -b branch3 master && commit_file newfile newfile ' write_script .git/EDITOR <<\EOF #!/bin/sh cat > "$1" < actual && echo refs/heads/pu > expect && test_cmp expect actual ' test_expect_success 'conflict in last branch resolved' ' test_must_fail git reintegrate --rebuild && git merge-base --is-ancestor branch1 HEAD && test_must_fail git merge-base --is-ancestor branch2 HEAD && echo resolved > base && git add base && git reintegrate --continue > output && cat output && grep -q branch2 output && git merge-base --is-ancestor branch2 HEAD ' test_expect_success 'conflict in last branch try continue when unresolved' ' test_must_fail git reintegrate --rebuild && git merge-base --is-ancestor branch1 HEAD && test_must_fail git merge-base --is-ancestor branch2 HEAD && test_must_fail git reintegrate --continue && echo resolved > base && git add base && git reintegrate --continue > output && cat output && grep -q branch2 output && git merge-base --is-ancestor branch2 HEAD ' test_expect_success 'conflict in last branch and abort' ' git checkout pu && git reset --hard master && test_must_fail git reintegrate --rebuild && git merge-base --is-ancestor branch1 HEAD && test_must_fail git merge-base --is-ancestor branch2 HEAD && git reintegrate --abort && git rev-parse --verify master > expect && git rev-parse --verify pu > actual && test_cmp expect actual && echo refs/heads/pu > expect && git symbolic-ref HEAD > actual && test_cmp expect actual && test_must_fail git merge-base --is-ancestor branch1 HEAD && test_must_fail git merge-base --is-ancestor branch2 HEAD ' test_expect_success 'abort does not move other branches' ' git checkout pu && git reset --hard master && git rev-parse --verify branch1 > expect && test_must_fail git reintegrate --rebuild && git checkout --force branch1 && git reintegrate --abort && git rev-parse --verify branch1 > actual && test_cmp expect actual ' write_script .git/EDITOR <<\EOF #!/bin/sh cat >> "$1" < base && git add base && git reintegrate --continue > output && cat output && grep -q branch2 output && grep -q branch3 output && git merge-base --is-ancestor branch2 HEAD && git merge-base --is-ancestor branch3 HEAD ' test_done git-reintegrate-0.4/test/fixup.t000077500000000000000000000027771271642736200170000ustar00rootroot00000000000000#!/bin/sh # # Copyright (C) 2013-2014 Felipe Contreras # Copyright (C) 2013 John Keeping # # This file may be used under the terms of the GNU GPL version 2. # test_description='Test the "fixup" instruction' . ./test-lib.sh test_expect_success 'setup branches' ' git init -q && commit_file base base && git checkout -b branch1 && commit_file branch1 branch1 && git checkout -b branch2 master && commit_file branch2 branch2 ' write_script .git/EDITOR <<\EOF #!/bin/sh cat >> "$1" <> "$1" < actual && echo refs/heads/pu > expect && test_cmp expect actual && git merge-base --is-ancestor branch1 HEAD && git merge-base --is-ancestor branch2 HEAD ' test_expect_success 'check fixup applied' ' echo fixup > expect && test_cmp expect fixup ' test_expect_success 'check fixup squashed' ' git log --oneline -- fixup > actual && grep "branch .branch2. into pu" actual && test $(wc -l &2 "Expected only a single commit touching fixup" exit 1 ) ' test_done git-reintegrate-0.4/test/reintegrate.t000077500000000000000000000073611271642736200201500ustar00rootroot00000000000000#!/bin/sh # # Copyright (C) 2013-2014 Felipe Contreras # Copyright (C) 2013 John Keeping # # This file may be used under the terms of the GNU GPL version 2. # test_description="Test git reintegrate" . ./test-lib.sh test_expect_success 'setup branches' ' git init -q && commit_file base base && git checkout -b branch1 master && commit_file branch1 branch1 && git checkout -b branch2 master && commit_file branch2 branch2 ' test_expect_success 'create integration branch' ' git checkout master && git reintegrate --create pu && git reintegrate --cat > actual && echo "base master" > expect && test_cmp expect actual && git symbolic-ref HEAD > actual && echo refs/heads/pu > expect && test_cmp expect actual ' write_script .git/EDITOR <<\EOF #!/bin/sh cat >> "$1" < expected ) && git log --merges --format="* %B" > actual && test_cmp expected actual } test_expect_success 'add branches to integration branch' ' GIT_EDITOR=".git/EDITOR" git reintegrate --edit && git reintegrate --rebuild && git merge-base --is-ancestor branch1 HEAD && git merge-base --is-ancestor branch2 HEAD && test_must_fail git merge-base --is-ancestor branch3 HEAD && check_int pu <<-EOF branch2:This merges branch 2. branch1:This merges branch 1. EOF ' write_script .git/EDITOR <<\EOF #!/bin/sh cat >> "$1" < expect && GIT_EDITOR=true git reintegrate --edit && git rev-parse --verify refs/int/pu > actual && test_cmp expect actual ' test_expect_success 'generate instructions' ' git init -q tmp && test_when_finished "rm -rf tmp" && ( cd tmp && commit_file base base && git checkout -b branch1 master && commit_file branch1 branch1 && git checkout -b branch2 master && commit_file branch2 branch2 && git checkout -b branch3 master && commit_file branch3 branch3 && git checkout -b pu master && git merge --no-ff branch1 && git merge --no-ff branch2 && git merge --no-ff branch3 && git checkout branch1 && commit_file branch1 branch1-update && git reintegrate --generate pu master && git reintegrate --cat pu > ../actual ) && cat > expected <<-EOF && base master merge branch1~1 merge branch2 merge branch3 EOF test_cmp expected actual ' write_script .git/EDITOR <<\EOF #!/bin/sh cat >> "$1" < actual && cat > expected <<-EOF && Empty commit. EOF test_cmp expected actual ' write_script .git/EDITOR <<\EOF #!/bin/sh cat > "$1" </dev/null 2>&1 && tput setaf 1 >/dev/null 2>&1 && tput sgr0 >/dev/null 2>&1 ) && color=t while test "$#" -ne 0; do case "$1" in -d|--d|--de|--deb|--debu|--debug) debug=t; shift ;; -i|--i|--im|--imm|--imme|--immed|--immedi|--immedia|--immediat|--immediate) immediate=t; shift ;; -l|--l|--lo|--lon|--long|--long-|--long-t|--long-te|--long-tes|--long-test|--long-tests) TEST_LONG=t; export TEST_LONG; shift ;; -h|--h|--he|--hel|--help) help=t; shift ;; -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose) verbose=t; shift ;; -q|--q|--qu|--qui|--quie|--quiet) # Ignore --quiet under a TAP::Harness. Saying how many tests # passed without the ok/not ok details is always an error. test -z "$HARNESS_ACTIVE" && quiet=t; shift ;; --no-color) color=; shift ;; --root=*) root=$(expr "z$1" : 'z[^=]*=\(.*\)') shift ;; *) echo "error: unknown test option '$1'" >&2; exit 1 ;; esac done if test -n "$color"; then say_color() { ( TERM=$ORIGINAL_TERM export TERM case "$1" in error) tput bold; tput setaf 1;; # bold red skip) tput setaf 4;; # blue warn) tput setaf 3;; # brown/yellow pass) tput setaf 2;; # green info) tput setaf 6;; # cyan *) test -n "$quiet" && return;; esac shift printf "%s" "$*" tput sgr0 echo ) } else say_color() { test -z "$1" && test -n "$quiet" && return shift printf "%s\n" "$*" } fi error() { say_color error "error: $*" EXIT_OK=t exit 1 } say() { say_color info "$*" } test -n "$test_description" || error "Test script did not set test_description." if test "$help" = "t"; then echo "$test_description" exit 0 fi exec 5>&1 exec 6<&0 if test "$verbose" = "t"; then exec 4>&2 3>&1 else exec 4>/dev/null 3>/dev/null fi test_failure=0 test_count=0 test_fixed=0 test_broken=0 test_success=0 die() { code=$? if test -n "$EXIT_OK"; then exit $code else echo >&5 "FATAL: Unexpected exit with code $code" exit 1 fi } EXIT_OK= trap 'die' EXIT # Public: Define that a test prerequisite is available. # # The prerequisite can later be checked explicitly using test_have_prereq or # implicitly by specifying the prerequisite name in calls to test_expect_success # or test_expect_failure. # # $1 - Name of prerequiste (a simple word, in all capital letters by convention) # # Examples # # # Set PYTHON prerequisite if interpreter is available. # command -v python >/dev/null && test_set_prereq PYTHON # # # Set prerequisite depending on some variable. # test -z "$NO_GETTEXT" && test_set_prereq GETTEXT # # Returns nothing. test_set_prereq() { satisfied_prereq="$satisfied_prereq$1 " } satisfied_prereq=" " # Public: Check if one or more test prerequisites are defined. # # The prerequisites must have previously been set with test_set_prereq. # The most common use of this is to skip all the tests if some essential # prerequisite is missing. # # $1 - Comma-separated list of test prerequisites. # # Examples # # # Skip all remaining tests if prerequisite is not set. # if ! test_have_prereq PERL; then # skip_all='skipping perl interface tests, perl not available' # test_done # fi # # Returns 0 if all prerequisites are defined or 1 otherwise. test_have_prereq() { # prerequisites can be concatenated with ',' save_IFS=$IFS IFS=, set -- $* IFS=$save_IFS total_prereq=0 ok_prereq=0 missing_prereq= for prerequisite; do case "$prerequisite" in !*) negative_prereq=t prerequisite=${prerequisite#!} ;; *) negative_prereq= esac total_prereq=$(($total_prereq + 1)) case "$satisfied_prereq" in *" $prerequisite "*) satisfied_this_prereq=t ;; *) satisfied_this_prereq= esac case "$satisfied_this_prereq,$negative_prereq" in t,|,t) ok_prereq=$(($ok_prereq + 1)) ;; *) # Keep a list of missing prerequisites; restore # the negative marker if necessary. prerequisite=${negative_prereq:+!}$prerequisite if test -z "$missing_prereq"; then missing_prereq=$prerequisite else missing_prereq="$prerequisite,$missing_prereq" fi esac done test $total_prereq = $ok_prereq } # You are not expected to call test_ok_ and test_failure_ directly, use # the text_expect_* functions instead. test_ok_() { test_success=$(($test_success + 1)) say_color "" "ok $test_count - $@" } test_failure_() { test_failure=$(($test_failure + 1)) say_color error "not ok $test_count - $1" shift echo "$@" | sed -e 's/^/# /' test "$immediate" = "" || { EXIT_OK=t; exit 1; } } test_known_broken_ok_() { test_fixed=$(($test_fixed + 1)) say_color error "ok $test_count - $@ # TODO known breakage vanished" } test_known_broken_failure_() { test_broken=$(($test_broken + 1)) say_color warn "not ok $test_count - $@ # TODO known breakage" } # Public: Execute commands in debug mode. # # Takes a single argument and evaluates it only when the test script is started # with --debug. This is primarily meant for use during the development of test # scripts. # # $1 - Commands to be executed. # # Examples # # test_debug "cat some_log_file" # # Returns the exit code of the last command executed in debug mode or 0 # otherwise. test_debug() { test "$debug" = "" || eval "$1" } test_eval_() { # This is a separate function because some tests use # "return" to end a test_expect_success block early. eval &3 2>&4 "$*" } test_run_() { test_cleanup=: expecting_failure=$2 test_eval_ "$1" eval_ret=$? if test -z "$immediate" || test $eval_ret = 0 || test -n "$expecting_failure"; then test_eval_ "$test_cleanup" fi if test "$verbose" = "t" && test -n "$HARNESS_ACTIVE"; then echo "" fi return "$eval_ret" } test_skip_() { test_count=$(($test_count + 1)) to_skip= for skp in $SKIP_TESTS; do case $this_test.$test_count in $skp) to_skip=t break esac done if test -z "$to_skip" && test -n "$test_prereq" && ! test_have_prereq "$test_prereq"; then to_skip=t fi case "$to_skip" in t) of_prereq= if test "$missing_prereq" != "$test_prereq"; then of_prereq=" of $test_prereq" fi say_color skip >&3 "skipping test: $@" say_color skip "ok $test_count # skip $1 (missing $missing_prereq${of_prereq})" : true ;; *) false ;; esac } # Public: Run test commands and expect them to succeed. # # When the test passed, an "ok" message is printed and the number of successful # tests is incremented. When it failed, a "not ok" message is printed and the # number of failed tests is incremented. # # With --immediate, exit test immediately upon the first failed test. # # Usually takes two arguments: # $1 - Test description # $2 - Commands to be executed. # # With three arguments, the first will be taken to be a prerequisite: # $1 - Comma-separated list of test prerequisites. The test will be skipped if # not all of the given prerequisites are set. To negate a prerequisite, # put a "!" in front of it. # $2 - Test description # $3 - Commands to be executed. # # Examples # # test_expect_success \ # 'git-write-tree should be able to write an empty tree.' \ # 'tree=$(git-write-tree)' # # # Test depending on one prerequisite. # test_expect_success TTY 'git --paginate rev-list uses a pager' \ # ' ... ' # # # Multiple prerequisites are separated by a comma. # test_expect_success PERL,PYTHON 'yo dawg' \ # ' test $(perl -E 'print eval "1 +" . qx[python -c "print 2"]') == "4" ' # # Returns nothing. test_expect_success() { test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_success" export test_prereq if ! test_skip_ "$@"; then say >&3 "expecting success: $2" if test_run_ "$2"; then test_ok_ "$1" else test_failure_ "$@" fi fi echo >&3 "" } # Public: Run test commands and expect them to fail. Used to demonstrate a known # breakage. # # This is NOT the opposite of test_expect_success, but rather used to mark a # test that demonstrates a known breakage. # # When the test passed, an "ok" message is printed and the number of fixed tests # is incremented. When it failed, a "not ok" message is printed and the number # of tests still broken is incremented. # # Failures from these tests won't cause --immediate to stop. # # Usually takes two arguments: # $1 - Test description # $2 - Commands to be executed. # # With three arguments, the first will be taken to be a prerequisite: # $1 - Comma-separated list of test prerequisites. The test will be skipped if # not all of the given prerequisites are set. To negate a prerequisite, # put a "!" in front of it. # $2 - Test description # $3 - Commands to be executed. # # Returns nothing. test_expect_failure() { test "$#" = 3 && { test_prereq=$1; shift; } || test_prereq= test "$#" = 2 || error "bug in the test script: not 2 or 3 parameters to test_expect_failure" export test_prereq if ! test_skip_ "$@"; then say >&3 "checking known breakage: $2" if test_run_ "$2" expecting_failure; then test_known_broken_ok_ "$1" else test_known_broken_failure_ "$1" fi fi echo >&3 "" } # Public: Run command and ensure that it fails in a controlled way. # # Use it instead of "! ". For example, when dies due to a # segfault, test_must_fail diagnoses it as an error, while "! " would # mistakenly be treated as just another expected failure. # # This is one of the prefix functions to be used inside test_expect_success or # test_expect_failure. # # $1.. - Command to be executed. # # Examples # # test_expect_success 'complain and die' ' # do something && # do something else && # test_must_fail git checkout ../outerspace # ' # # Returns 1 if the command succeeded (exit code 0). # Returns 1 if the command died by signal (exit codes 130-192) # Returns 1 if the command could not be found (exit code 127). # Returns 0 otherwise. test_must_fail() { "$@" exit_code=$? if test $exit_code = 0; then echo >&2 "test_must_fail: command succeeded: $*" return 1 elif test $exit_code -gt 129 -a $exit_code -le 192; then echo >&2 "test_must_fail: died by signal: $*" return 1 elif test $exit_code = 127; then echo >&2 "test_must_fail: command not found: $*" return 1 fi return 0 } # Public: Run command and ensure that it succeeds or fails in a controlled way. # # Similar to test_must_fail, but tolerates success too. Use it instead of # " || :" to catch failures caused by a segfault, for instance. # # This is one of the prefix functions to be used inside test_expect_success or # test_expect_failure. # # $1.. - Command to be executed. # # Examples # # test_expect_success 'some command works without configuration' ' # test_might_fail git config --unset all.configuration && # do something # ' # # Returns 1 if the command died by signal (exit codes 130-192) # Returns 1 if the command could not be found (exit code 127). # Returns 0 otherwise. test_might_fail() { "$@" exit_code=$? if test $exit_code -gt 129 -a $exit_code -le 192; then echo >&2 "test_might_fail: died by signal: $*" return 1 elif test $exit_code = 127; then echo >&2 "test_might_fail: command not found: $*" return 1 fi return 0 } # Public: Run command and ensure it exits with a given exit code. # # This is one of the prefix functions to be used inside test_expect_success or # test_expect_failure. # # $1 - Expected exit code. # $2.. - Command to be executed. # # Examples # # test_expect_success 'Merge with d/f conflicts' ' # test_expect_code 1 git merge "merge msg" B master # ' # # Returns 0 if the expected exit code is returned or 1 otherwise. test_expect_code() { want_code=$1 shift "$@" exit_code=$? if test $exit_code = $want_code; then return 0 fi echo >&2 "test_expect_code: command exited with $exit_code, we wanted $want_code $*" return 1 } # Public: Compare two files to see if expected output matches actual output. # # The TEST_CMP variable defines the command used for the comparision; it # defaults to "diff -u". Only when the test script was started with --verbose, # will the command's output, the diff, be printed to the standard output. # # This is one of the prefix functions to be used inside test_expect_success or # test_expect_failure. # # $1 - Path to file with expected output. # $2 - Path to file with actual output. # # Examples # # test_expect_success 'foo works' ' # echo expected >expected && # foo >actual && # test_cmp expected actual # ' # # Returns the exit code of the command set by TEST_CMP. test_cmp() { ${TEST_CMP:-diff -u} "$@" } # Public: Schedule cleanup commands to be run unconditionally at the end of a # test. # # If some cleanup command fails, the test will not pass. With --immediate, no # cleanup is done to help diagnose what went wrong. # # This is one of the prefix functions to be used inside test_expect_success or # test_expect_failure. # # $1.. - Commands to prepend to the list of cleanup commands. # # Examples # # test_expect_success 'test core.capslock' ' # git config core.capslock true && # test_when_finished "git config --unset core.capslock" && # do_something # ' # # Returns the exit code of the last cleanup command executed. test_when_finished() { test_cleanup="{ $* } && (exit \"\$eval_ret\"); eval_ret=\$?; $test_cleanup" } # Public: Summarize test results and exit with an appropriate error code. # # Must be called at the end of each test script. # # Can also be used to stop tests early and skip all remaining tests. For this, # set skip_all to a string explaining why the tests were skipped before calling # test_done. # # Examples # # # Each test script must call test_done at the end. # test_done # # # Skip all remaining tests if prerequisite is not set. # if ! test_have_prereq PERL; then # skip_all='skipping perl interface tests, perl not available' # test_done # fi # # Returns 0 if all tests passed or 1 if there was a failure. test_done() { EXIT_OK=t if test -z "$HARNESS_ACTIVE"; then test_results_dir="$SHARNESS_TEST_DIRECTORY/test-results" mkdir -p "$test_results_dir" test_results_path="$test_results_dir/${SHARNESS_TEST_FILE%.$SHARNESS_TEST_EXTENSION}.$$.counts" cat >>"$test_results_path" <<-EOF total $test_count success $test_success fixed $test_fixed broken $test_broken failed $test_failure EOF fi if test "$test_fixed" != 0; then say_color error "# $test_fixed known breakage(s) vanished; please update test(s)" fi if test "$test_broken" != 0; then say_color warn "# still have $test_broken known breakage(s)" fi if test "$test_broken" != 0 || test "$test_fixed" != 0; then test_remaining=$(( $test_count - $test_broken - $test_fixed )) msg="remaining $test_remaining test(s)" else test_remaining=$test_count msg="$test_count test(s)" fi case "$test_failure" in 0) # Maybe print SKIP message if test -n "$skip_all" && test $test_count -gt 0; then error "Can't use skip_all after running some tests" fi [ -z "$skip_all" ] || skip_all=" # SKIP $skip_all" if test $test_remaining -gt 0; then say_color pass "# passed all $msg" fi say "1..$test_count$skip_all" test -d "$remove_trash" && cd "$(dirname "$remove_trash")" && rm -rf "$(basename "$remove_trash")" exit 0 ;; *) say_color error "# failed $test_failure among $msg" say "1..$test_count" exit 1 ;; esac } # Public: Root directory containing tests. Tests can override this variable, # e.g. for testing Sharness itself. : ${SHARNESS_TEST_DIRECTORY:=$(pwd)} export SHARNESS_TEST_DIRECTORY # Public: Build directory that will be added to PATH. By default, it is set to # the parent directory of SHARNESS_TEST_DIRECTORY. : ${SHARNESS_BUILD_DIRECTORY:="$SHARNESS_TEST_DIRECTORY/.."} PATH="$SHARNESS_BUILD_DIRECTORY:$PATH" export PATH SHARNESS_BUILD_DIRECTORY # Public: Path to test script currently executed. SHARNESS_TEST_FILE="$0" export SHARNESS_TEST_FILE # Prepare test area. test_dir="trash directory.$(basename "$SHARNESS_TEST_FILE" ".$SHARNESS_TEST_EXTENSION")" test -n "$root" && test_dir="$root/$test_dir" case "$test_dir" in /*) SHARNESS_TRASH_DIRECTORY="$test_dir" ;; *) SHARNESS_TRASH_DIRECTORY="$SHARNESS_TEST_DIRECTORY/$test_dir" ;; esac test "$debug" = "t" || remove_trash="$SHARNESS_TRASH_DIRECTORY" rm -rf "$test_dir" || { EXIT_OK=t echo >&5 "FATAL: Cannot prepare test area" exit 1 } # Public: Empty trash directory, the test area, provided for each test. The HOME # variable is set to that directory too. export SHARNESS_TRASH_DIRECTORY HOME="$SHARNESS_TRASH_DIRECTORY" export HOME mkdir -p "$test_dir" || exit 1 # Use -P to resolve symlinks in our working directory so that the cwd # in subprocesses like git equals our $PWD (for pathname comparisons). cd -P "$test_dir" || exit 1 this_test=${SHARNESS_TEST_FILE##*/} this_test=${this_test%.$SHARNESS_TEST_EXTENSION} for skp in $SKIP_TESTS; do case "$this_test" in $skp) say_color info >&3 "skipping test $this_test altogether" skip_all="skip all tests in $this_test" test_done esac done # vi: set ts=4 sw=4 noet : git-reintegrate-0.4/test/test-lib.sh000066400000000000000000000006551271642736200175250ustar00rootroot00000000000000#!/bin/sh . ./sharness.sh GIT_AUTHOR_EMAIL=author@example.com GIT_AUTHOR_NAME='A U Thor' GIT_COMMITTER_EMAIL=committer@example.com GIT_COMMITTER_NAME='C O Mitter' export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME commit_file() { local filename="$1" echo "$2" > $filename && git add -f $filename && git commit -q -m "commit $filename" } write_script() { cat > "$1" && chmod +x "$1" }