Skip to content

Instantly share code, notes, and snippets.

@the-michael-toy
Last active December 15, 2015 15:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save the-michael-toy/9907309 to your computer and use it in GitHub Desktop.
Save the-michael-toy/9907309 to your computer and use it in GitHub Desktop.
Try to create every possible state reportable by git-staus
# This tries to produce a git repository with a file in every possible state that git-status
# could return. A "remote" and "user" repostories are created with initially identical contents
# and then series of operations are performed to leave the user repository in a state where
# git-status reports one of each possible status.
#
# Note: As of now, I can only reproduce 17 of the 24 possible states.
require 'fileutils'
require 'securerandom'
require 'shellwords'
class GitRepository
attr_reader :dir
def initialize(repo_dir)
@dir = repo_dir
end
def method_missing(cmd, *args)
if %w(add status rm mv init).include? cmd.to_s
git cmd, *args
else
super
end
end
def git(gitcmd, *args)
sh = "git #{gitcmd}"
args.each {|arg| sh << " #{Shellwords::escape arg}" unless arg.nil? }
results = `cd #{Shellwords::escape @dir} && #{sh} 2>&1`
error = $?.exitstatus
raise "Error(#{error}): #{sh}\n#{results}" if error != 0
results
end
def pull
# a merge conflict will result in an error, which is fine
git :pull, "origin", "master" rescue nil
end
def commit(msg)
git :commit, "-m"+msg
end
def clone(dst)
git :clone, @dir, dst
end
end
class EveryStatus
def initialize
@dir = "/tmp/every_git_status_#{SecureRandom.hex(10)}"
FileUtils.mkdir @dir
@rdir = File.join(@dir, "remote")
@udir = File.join(@dir, "user")
FileUtils.mkdir @rdir
@remote_repo = GitRepository.new @rdir
@remote_repo.init
update @rdir, "afile", "here is a file in the repository"
@remote_repo.add "afile"
@remote_repo.commit "Initial commit"
@remote_repo.clone @udir
@user_repo = GitRepository.new @udir
end
def cleanup
FileUtils.rm_r @dir if @dir
end
def update(dir, file, str)
File.write(File.join(dir, file), str, 0, mode: 'a+')
end
def run
=begin
The following table is from "man git-status" and shows every possible return from
command-line git to a status query.
X Y Meaning
-------------------------------------------------
[MD] not updated
M [ MD] updated in index
A [ MD] added to index
D [ M] deleted from index
R [ MD] renamed in index
C [ MD] copied in index
[MARC] index and work tree matches
[ MARC] M work tree changed since index
[ MARC] D deleted in work tree
-------------------------------------------------
D D unmerged, both deleted
A U unmerged, added by us
U D unmerged, deleted by them
U A unmerged, added by them
D U unmerged, deleted by us
A A unmerged, both added
U U unmerged, both modified
-------------------------------------------------
? ? untracked
! ! ignored
-------------------------------------------------
To try and re-create these states, we write some code in a tiny languge
The execution goes through these steps:
1) Create master repo with all files from all tests
2) Clone master to user repo
3) Run all commands up to the first "_" in each recipe
4) Commit in both repos
5) Run all commands to next "_" from each recipe
6) Pull
7) Run remaining commands
At this point, the repository should have a file in every state for
which there is a recipe
The the tiny language uses single characters to indicate actions to take
on the two repositories.
Commands are:
! -- this file is not in the original clone
? -- no commands, don't know how to create this status
c -- commit this repo
m -- set repo to master
u -- set repo to user
w -- change to working copy
i -- change to index
d -- delete working copy
x --- remove from index (git rm)
n -- rename the file (git mv)
For example, the command string "miui__" will write new data to the master,
and to the user repo, and then commit that new data, and generate a merge
conflict in the final status after the pull.
=end
# recipes for producing each status, "???" means i don't know how to make that happen
recipes = {
"??" => "!__uw" , # untracked new file
" M" => "__uw" , # make some changes to a file
"M " => "__ui" , # make changes and add a file
"MM" => "__uiw" , # change, add, then change again
"MD" => "__uid" , # make changes and them rm file
"A " => "!__ui" , # add new file to index
"AM" => "!__uiw" , # add new file, then change
"AD" => "!__uid" , # add new file, then drop
" D" => "__ud" , # rm the file
"D " => "__ux" , # git rm the file
"DM" => "???" , # (git rm then, git add => "M " NOT "DM")
"R " => "__un" , # git mv to a new namw
"RM" => "__unw" , # rename file, then touch old
"RD" => "__und" , # rename file then delete it
"C " => "???" ,
"CM" => "???" ,
"CD" => "???" ,
"UU" => "miui__" , # standard double edit conflict
"DU" => "miux__" , # change v. delete
"UD" => "mxui__" , # delete v. change
"AA" => "!_micuic_" , # add new version to both repos
"DD" => "???" ,
"AU" => "???" ,
"UA" => "???" ,
}
file_to_order = {}
orders = recipes.each_pair.collect do |expect, cmds|
a = { expect: expect }
if cmds[0] != '?'
(a[:sync], a[:commit], a[:pull]) = cmds.split "_"
a[:file] = "status-test-#{cmds}"
file_to_order[a[:file]] = a
if cmds[0] != '!'
update @rdir, a[:file], "original version of #{a[:file]}\n"
@remote_repo.add a[:file]
end
end
a
end
@remote_repo.commit "Master and User in sync at this point"
@user_repo.pull
execute = lambda do |cmds, a|
return unless a[:file]
repo = @remote_repo
cmds ||= ""
cmds.each_char do |cmd|
case cmd
when 'm'
repo = @remote_repo
when 'u'
repo = @user_repo
when 'c'
repo.commit "commit in recipe"
when 'd'
File.delete File.join(repo.dir, a[:file])
when 'x'
repo.rm a[:file]
when 'n'
mvfile = a[:file] + ".mv"
repo.mv a[:file], mvfile
a[:old_file] = a[:file]
a[:file] = mvfile
when 'i'
update repo.dir, a[:file], SecureRandom.uuid + "\n"
repo.add a[:file]
when 'w'
update repo.dir, a[:file], SecureRandom.uuid + "\n"
when '!'
else
raise "tiny language no have verb '#{cmd}' in #{cmds}"
end
end
end
orders.each { |a| execute.call(a[:sync], a) }
@user_repo.commit "pre merge changes to the user repo"
@remote_repo.commit "pre merge changes to the master repo"
orders.each { |a| execute.call(a[:commit], a) }
@user_repo.pull
orders.each { |a| execute.call(a[:pull], a) }
# Having run all the recipes, at this point the @user_repo should have a file with each possible status
report = recipes.clone
git_status = (@user_repo.status "--porcelain","-z").split("\000")
while (one_status = git_status.shift)
pieces = one_status.match /^(..) (.*$)/
name = pieces[2]
status = pieces[1]
if status[0] == "R"
old_name = pieces[2]
name = git_status.shift
end
if file_to_order[name]
if file_to_order[name][:expect] != status
puts "ERROR -- #{name} expected status #{file_to_order[name][:expect]}, got #{status}"
else
puts "... '#{status}' for file #{name} is correct"
report.delete status
end
end
end
report.keys.each do |missing_status|
how = recipes[missing_status]
if how[0] == '?'
puts "... '#{missing_status}' not generated, no recipe given"
else
puts "ERROR '#{missing_status}' did not result from #{how}"
end
end
end
end
n = EveryStatus.new
begin
n.run
ensure
n.cleanup
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment