Last active
December 15, 2015 15:41
-
-
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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