Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 261 lines (224 sloc) 6.73 KB
#!/usr/bin/env python
# coding: utf-8
# git-darcs-record, emulate "darcs record" interface on top of a git repository
#
# Usage:
# git-darcs-record first asks for any new file (previously
# untracked) to be added to the index.
# git-darcs-record then asks for each hunk to be recorded in
# the next commit. File deletion and binary blobs are supported
# git-darcs-record finally asks for a small commit message and
# executes the 'git commit' command with the newly created
# changeset in the index
# Copyright (C) 2007 Raphaël Slinckx <raphael@slinckx.net>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import re, pprint, sys, os
BINARY = re.compile("GIT binary patch")
HEADER = re.compile("diff --git a/(.*) b/(.*)")
class Hunk:
def __init__(self, lines, binary):
self.diff = None
self.lines = lines
self.keep = False
self.binary = binary
def format(self):
output = self.diff.header.modified + "\n"
if self.diff.header.deleted:
output = "Removed file: " + output
if self.binary:
output = "Binary file changed: " + output
if not self.binary:
output += "\n".join(self.lines) + "\n"
return output
class Header:
def __init__(self, lines):
self.lines = lines
self.modified = None
self.deleted = False
# Extract useful info from header from git
for line in lines:
if HEADER.match(line):
match = HEADER.match(line)
self.modified = match.group(1)
if line.startswith("deleted "):
self.deleted = True
# Make sure we know what file we are modifying
assert self.modified
class Diff:
def __init__(self, header, hunks):
self.header = header
self.hunks = hunks
# Put a reference to ourselves in the hunks
for hunk in self.hunks:
hunk.diff = self
self.keep = False
def filter(self):
output = '\n'.join(self.header.lines) + "\n"
for hunk in self.hunks:
if not hunk.keep:
continue
output += '\n'.join(hunk.lines) + "\n"
return output
@classmethod
def filter_diffs(kls, diffs):
output = ""
for diff in diffs:
if not diff.keep:
continue
output += diff.filter()
return output
@classmethod
def parse(kls, lines):
in_header = True
binary = False
header = []
hunks = []
current_hunk = []
for line in lines:
if in_header and (line[0] not in (" ", "@", "\\") or line.startswith("+++ ") or line.startswith("--- ")) and not BINARY.match(line):
header.append(line)
elif BINARY.match(line):
in_header = False
binary = True
header.append(line)
elif len(line) >= 1 and line[0] == "@":
in_header = False
if current_hunk:
hunks.append(Hunk(current_hunk, binary))
current_hunk = []
current_hunk.append(line)
else:
current_hunk.append(line)
if current_hunk:
hunks.append(Hunk(current_hunk, binary))
return Diff(Header(header), hunks)
@classmethod
def split(kls, lines):
diffs = []
current_diff = []
for line in lines:
if line.startswith("diff --git "):
if current_diff:
diffs.append(current_diff)
current_diff = []
current_diff.append(line)
else:
current_diff.append(line)
if current_diff:
diffs.append(current_diff)
return [Diff.parse(lines) for lines in diffs]
def read_answer(question, allowed_responses=["Y", "n", "d", "a"]):
#Make sure there is alway a default selection
assert [r for r in allowed_responses if r.isupper()]
while True:
resp = raw_input("%s [%s] : " % (question, "".join(allowed_responses)))
if resp in [r.lower() for r in allowed_responses]:
break
elif resp == "":
resp = [r for r in allowed_responses if r.isupper()][0].lower()
break
print 'Unexpected answer: %r' % resp
return resp
def setup_git_dir():
global GIT_DIR
GIT_DIR = os.getcwd()
while not os.path.exists(os.path.join(GIT_DIR, ".git")):
GIT_DIR = os.path.dirname(GIT_DIR)
if GIT_DIR == "/":
return False
os.chdir(GIT_DIR)
return True
def git_get_untracked_files():
return [f.strip() for f in os.popen("git ls-files --others --exclude-from='%s' --exclude-per-directory=.gitignore" % (os.path.join(GIT_DIR, ".git", "info", "exclude"))).readlines()]
def git_track_file(f):
os.spawnvp(os.P_WAIT, "git", ["git", "add", f])
def git_diff():
return os.popen("git diff -u --no-color --binary").readlines()
def git_apply(patch):
stdin, stdout = os.popen2(["git", "apply", "--cached", "-"])
stdin.write(patch)
stdin.close()
output = stdout.read()
stdout.close()
os.wait()
return output
def git_status():
os.spawnvp(os.P_WAIT, "git", ["git", "status"])
def git_commit(msg):
os.spawnvp(os.P_WAIT, "git", ["git", "commit", "-m", patch_name])
# Main loop ------------------------
if not setup_git_dir():
print "Must be in a git (sub-)directory! Exiting..."
sys.exit()
# Ask for new files ----------------
git_untracked_files = git_get_untracked_files()
git_track_files = []
all = False
done = False
for i, f in enumerate(git_untracked_files):
if not all:
print "Add file: ", f
resp = read_answer("Shall I add this file? (%d/%d)" % (i+1, len(git_untracked_files)))
else:
resp = "y"
if resp == "y":
git_track_files.append(f)
elif resp == "a":
git_track_files.append(f)
all = True
elif resp == "d":
done = True
break
# Ask for each hunk of the diff
diffs = Diff.split([line[:-1] for line in git_diff()])
total_hunks = sum([len(diff.hunks) for diff in diffs])
n_hunk = 1
for diff in diffs:
if done:
break
for hunk in diff.hunks:
# Check if we are in override mode
if not all:
print
print hunk.format()
resp = read_answer('Shall I record this change? (%d/%d)' % (n_hunk, total_hunks))
# Otherwise say 'y' to all remaining patches
else:
resp = "y"
if resp == "y":
diff.keep = True
hunk.keep = True
elif resp == "a":
diff.keep = True
hunk.keep = True
all = True
elif resp == "d":
done = True
break
n_hunk += 1
# Add new files to track
for f in git_track_files:
git_track_file(f)
# Generate a new patch to be used with git apply
new_patch = Diff.filter_diffs(diffs)
if new_patch:
print git_apply(new_patch)
if new_patch or git_track_files:
git_status()
patch_name = raw_input("What is the patch name? ")
git_commit(patch_name)
else:
print "Ok, if you don't want to record anything, that's fine!"