"""
import sys, os, popen2, re, gitmergeonefile
+from shutil import copyfile
from stgit import basedir
from stgit.utils import *
"""An author, committer, etc."""
def __init__(self, name = None, email = None, date = '',
desc = None):
+ self.name = self.email = self.date = None
if name or email or date:
assert not desc
self.name = name
def __init__(self, id_hash):
self.__id_hash = id_hash
- lines = _output_lines('git-cat-file commit %s' % id_hash)
+ lines = _output_lines(['git-cat-file', 'commit', id_hash])
for i in range(len(lines)):
line = lines[i]
if line == '\n':
return None
def get_parents(self):
- return _output_lines('git-rev-list --parents --max-count=1 %s'
- % self.__id_hash)[0].split()[1:]
+ return _output_lines(['git-rev-list', '--parents', '--max-count=1',
+ self.__id_hash])[0].split()[1:]
def get_author(self):
return self.__author
p.tochild.write(line)
p.tochild.close()
if p.wait():
- raise GitException, '%s failed (%s)' % (str(cmd),
+ raise GitException, '%s failed (%s)' % (' '.join(cmd),
p.childerr.read().strip())
def _input_str(cmd, string):
p.tochild.write(string)
p.tochild.close()
if p.wait():
- raise GitException, '%s failed (%s)' % (str(cmd),
+ raise GitException, '%s failed (%s)' % (' '.join(cmd),
p.childerr.read().strip())
def _output(cmd):
p=popen2.Popen3(cmd, True)
output = p.fromchild.read()
if p.wait():
- raise GitException, '%s failed (%s)' % (str(cmd),
+ raise GitException, '%s failed (%s)' % (' '.join(cmd),
p.childerr.read().strip())
return output
p.tochild.close()
output = p.fromchild.readline().strip()
if p.wait():
- raise GitException, '%s failed (%s)' % (str(cmd),
+ raise GitException, '%s failed (%s)' % (' '.join(cmd),
p.childerr.read().strip())
return output
p=popen2.Popen3(cmd, True)
lines = p.fromchild.readlines()
if p.wait():
- raise GitException, '%s failed (%s)' % (str(cmd),
+ raise GitException, '%s failed (%s)' % (' '.join(cmd),
p.childerr.read().strip())
return lines
return r
return 0
-def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
- noexclude = True, verbose = False):
+def tree_status(files = None, tree_id = 'HEAD', unknown = False,
+ noexclude = True, verbose = False, diff_flags = []):
"""Returns a list of pairs - [status, filename]
"""
- if verbose and sys.stdout.isatty():
- print 'Checking for changes in the working directory...',
- sys.stdout.flush()
+ if verbose:
+ out.start('Checking for changes in the working directory')
refresh_index()
cache_files += [('C', filename) for filename in conflicts]
# the rest
- for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
+ for line in _output_lines(['git-diff-index'] + diff_flags +
+ [ tree_id, '--'] + files):
fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
if fs[1] not in conflicts:
cache_files.append(fs)
- if verbose and sys.stdout.isatty():
- print 'done'
+ if verbose:
+ out.done()
return cache_files
def local_changes(verbose = True):
"""Return true if there are local changes in the tree
"""
- return len(__tree_status(verbose = verbose)) != 0
+ return len(tree_status(verbose = verbose)) != 0
# HEAD value cached
__head = None
"""Returns the name of the file pointed to by the HEAD link
"""
return strip_prefix('refs/heads/',
- _output_one_line('git-symbolic-ref HEAD'))
+ _output_one_line(['git-symbolic-ref', 'HEAD']))
def set_head_file(ref):
"""Resets HEAD to point to a new ref
[os.path.join('refs', 'heads', ref)]) != 0:
raise GitException, 'Could not set head to "%s"' % ref
+def set_branch(branch, val):
+ """Point branch at a new commit object."""
+ if __run('git-update-ref', [branch, val]) != 0:
+ raise GitException, 'Could not update %s to "%s".' % (branch, val)
+
def __set_head(val):
"""Sets the HEAD value
"""
global __head
if not __head or __head != val:
- if __run('git-update-ref HEAD', [val]) != 0:
- raise GitException, 'Could not update HEAD to "%s".' % val
+ set_branch('HEAD', val)
__head = val
# only allow SHA1 hashes
if __run('git-update-index --add --', files):
raise GitException, 'Unable to add file'
+def __copy_single(source, target, target2=''):
+ """Copy file or dir named 'source' to name target+target2"""
+
+ # "source" (file or dir) must match one or more git-controlled file
+ realfiles = _output_lines(['git-ls-files', source])
+ if len(realfiles) == 0:
+ raise GitException, '"%s" matches no git-controled files' % source
+
+ if os.path.isdir(source):
+ # physically copy the files, and record them to add them in one run
+ newfiles = []
+ re_string='^'+source+'/(.*)$'
+ prefix_regexp = re.compile(re_string)
+ for f in [f.strip() for f in realfiles]:
+ m = prefix_regexp.match(f)
+ if not m:
+ raise Exception, '"%s" does not match "%s"' % (f, re_string)
+ newname = target+target2+'/'+m.group(1)
+ if not os.path.exists(os.path.dirname(newname)):
+ os.makedirs(os.path.dirname(newname))
+ copyfile(f, newname)
+ newfiles.append(newname)
+
+ add(newfiles)
+ else: # files, symlinks, ...
+ newname = target+target2
+ copyfile(source, newname)
+ add([newname])
+
+
+def copy(filespecs, target):
+ if os.path.isdir(target):
+ # target is a directory: copy each entry on the command line,
+ # with the same name, into the target
+ target = target.rstrip('/')
+
+ # first, check that none of the children of the target
+ # matching the command line aleady exist
+ for filespec in filespecs:
+ entry = target+ '/' + os.path.basename(filespec.rstrip('/'))
+ if os.path.exists(entry):
+ raise GitException, 'Target "%s" already exists' % entry
+
+ for filespec in filespecs:
+ filespec = filespec.rstrip('/')
+ basename = '/' + os.path.basename(filespec)
+ __copy_single(filespec, target, basename)
+
+ elif os.path.exists(target):
+ raise GitException, 'Target "%s" exists but is not a directory' % target
+ elif len(filespecs) != 1:
+ raise GitException, 'Cannot copy more than one file to non-directory'
+
+ else:
+ # at this point: len(filespecs)==1 and target does not exist
+
+ # check target directory
+ targetdir = os.path.dirname(target)
+ if targetdir != '' and not os.path.isdir(targetdir):
+ raise GitException, 'Target directory "%s" does not exist' % targetdir
+
+ __copy_single(filespecs[0].rstrip('/'), target)
+
+
def rm(files, force = False):
"""Remove a file from the repository
"""
if not __user:
name=config.get('user.name')
email=config.get('user.email')
- if name and email:
- __user = Person(name, email)
- else:
- raise GitException, 'unknown user details'
+ __user = Person(name, email)
return __user;
def author():
if not files:
files = []
- cache_files = __tree_status(files, verbose = False)
+ cache_files = tree_status(files, verbose = False)
# everything is up-to-date
if len(cache_files) == 0:
must_switch = True
# write the index to repository
if tree_id == None:
- tree_id = _output_one_line('git-write-tree')
+ tree_id = _output_one_line(['git-write-tree'])
else:
must_switch = False
# the commit
- cmd = ''
+ cmd = ['env']
if author_name:
- cmd += 'GIT_AUTHOR_NAME="%s" ' % author_name
+ cmd += ['GIT_AUTHOR_NAME=%s' % author_name]
if author_email:
- cmd += 'GIT_AUTHOR_EMAIL="%s" ' % author_email
+ cmd += ['GIT_AUTHOR_EMAIL=%s' % author_email]
if author_date:
- cmd += 'GIT_AUTHOR_DATE="%s" ' % author_date
+ cmd += ['GIT_AUTHOR_DATE=%s' % author_date]
if committer_name:
- cmd += 'GIT_COMMITTER_NAME="%s" ' % committer_name
+ cmd += ['GIT_COMMITTER_NAME=%s' % committer_name]
if committer_email:
- cmd += 'GIT_COMMITTER_EMAIL="%s" ' % committer_email
- cmd += 'git-commit-tree %s' % tree_id
+ cmd += ['GIT_COMMITTER_EMAIL=%s' % committer_email]
+ cmd += ['git-commit-tree', tree_id]
# get the parents
for p in parents:
- cmd += ' -p %s' % p
+ cmd += ['-p', p]
commit_id = _output_one_line(cmd, message)
if must_switch:
the pushing would fall back to the three-way merge.
"""
if check_index:
- index_opt = '--index'
+ index_opt = ['--index']
else:
- index_opt = ''
+ index_opt = []
if not files:
files = []
diff_str = diff(files, rev1, rev2)
if diff_str:
try:
- _input_str('git-apply %s' % index_opt, diff_str)
+ _input_str(['git-apply'] + index_opt, diff_str)
except GitException:
return False
"""
refresh_index()
+ err_output = None
if recursive:
# this operation tracks renames but it is slower (used in
# general when pushing or picking patches)
try:
# use _output() to mask the verbose prints of the tool
- _output('git-merge-recursive %s -- %s %s' % (base, head1, head2))
- except GitException:
+ _output(['git-merge-recursive', base, '--', head1, head2])
+ except GitException, ex:
+ err_output = str(ex)
pass
else:
# the fast case where we don't track renames (used when the
files = {}
stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S)
- for line in _output('git-ls-files --unmerged --stage -z').split('\0'):
+ for line in _output(['git-ls-files', '--unmerged', '--stage', '-z']).split('\0'):
if not line:
continue
files[path][stage] = (mode, hash)
+ if err_output and not files:
+ # if no unmerged files, there was probably a different type of
+ # error and we have to abort the merge
+ raise GitException, err_output
+
# merge the unmerged files
errors = False
for path in files:
raise GitException, 'GIT index merging failed (possible conflicts)'
def status(files = None, modified = False, new = False, deleted = False,
- conflict = False, unknown = False, noexclude = False):
+ conflict = False, unknown = False, noexclude = False,
+ diff_flags = []):
"""Show the tree status
"""
if not files:
files = []
- cache_files = __tree_status(files, unknown = True, noexclude = noexclude)
+ cache_files = tree_status(files, unknown = True, noexclude = noexclude,
+ diff_flags = diff_flags)
all = not (modified or new or deleted or conflict or unknown)
if not all:
if files and not fs[1] in files:
continue
if all:
- print '%s %s' % (fs[0], fs[1])
+ out.stdout('%s %s' % (fs[0], fs[1]))
else:
- print '%s' % fs[1]
+ out.stdout('%s' % fs[1])
-def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None):
+def diff(files = None, rev1 = 'HEAD', rev2 = None, out_fd = None,
+ diff_flags = []):
"""Show the diff between rev1 and rev2
"""
if not files:
files = []
if rev1 and rev2:
- diff_str = _output(['git-diff-tree', '-p', rev1, rev2, '--'] + files)
+ diff_str = _output(['git-diff-tree', '-p'] + diff_flags
+ + [rev1, rev2, '--'] + files)
elif rev1 or rev2:
refresh_index()
if rev2:
- diff_str = _output(['git-diff-index', '-p', '-R', rev2, '--'] + files)
+ diff_str = _output(['git-diff-index', '-p', '-R']
+ + diff_flags + [rev2, '--'] + files)
else:
- diff_str = _output(['git-diff-index', '-p', rev1, '--'] + files)
+ diff_str = _output(['git-diff-index', '-p']
+ + diff_flags + [rev1, '--'] + files)
else:
diff_str = ''
raise GitException, 'git.diffstat failed'
return diff_str
-def files(rev1, rev2):
+def files(rev1, rev2, diff_flags = []):
"""Return the files modified between rev1 and rev2
"""
result = ''
- for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
+ for line in _output_lines(['git-diff-tree'] + diff_flags + ['-r', rev1, rev2]):
result += '%s %s\n' % tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
return result.rstrip()
"""
result = ''
- for line in _output_lines('git-diff-tree -r %s %s' % (rev1, rev2)):
+ for line in _output_lines(['git-diff-tree', '-r', rev1, rev2]):
result += '%s\n' % line.rstrip().split(' ',4)[-1].split('\t',1)[-1]
return result.rstrip()
-def pretty_commit(commit_id = 'HEAD'):
+def pretty_commit(commit_id = 'HEAD', diff_flags = []):
"""Return a given commit (log + diff)
"""
- return _output(['git-diff-tree', '--cc', '--always', '--pretty', '-r',
- commit_id])
+ return _output(['git-diff-tree'] + diff_flags +
+ ['--cc', '--always', '--pretty', '-r', commit_id])
def checkout(files = None, tree_id = None, force = False):
"""Check out the given or all files
tree_id = get_head()
if check_out:
- cache_files = __tree_status(files, tree_id)
+ cache_files = tree_status(files, tree_id)
# files which were added but need to be removed
rm_files = [x[1] for x in cache_files if x[0] in ['A']]
if refspec:
args.append(refspec)
- command = config.get('stgit.pullcmd')
+ command = config.get('branch.%s.stgit.fetchcmd' % get_head_file()) or \
+ config.get('stgit.fetchcmd')
+ if __run(command, args) != 0:
+ raise GitException, 'Failed "%s %s"' % (command, repository)
+
+def pull(repository = 'origin', refspec = None):
+ """Fetches changes from the remote repository, using 'git-pull'
+ by default.
+ """
+ # we update the HEAD
+ __clear_head_cache()
+
+ args = [repository]
+ if refspec:
+ args.append(refspec)
+
+ command = config.get('branch.%s.stgit.pullcmd' % get_head_file()) or \
+ config.get('stgit.pullcmd')
if __run(command, args) != 0:
raise GitException, 'Failed "%s %s"' % (command, repository)
refresh_index()
try:
- _input_str('git-apply --index', diff)
+ _input_str(['git-apply', '--index'], diff)
except GitException:
if base:
switch(orig_head)
f = file('.stgit-failed.patch', 'w+')
f.write(diff)
f.close()
- print >> sys.stderr, 'Diff written to the .stgit-failed.patch file'
+ out.warn('Diff written to the .stgit-failed.patch file')
raise
raise GitException, 'Failed "git-clone %s %s"' \
% (repository, local_dir)
-def modifying_revs(files, base_rev):
+def modifying_revs(files, base_rev, head_rev):
"""Return the revisions from the list modifying the given files
"""
- cmd = ['git-rev-list', '%s..' % base_rev, '--']
+ cmd = ['git-rev-list', '%s..%s' % (base_rev, head_rev), '--']
revs = [line.strip() for line in _output_lines(cmd + files)]
return revs
return config.sections_matching(r'remote\.(.*)\.url')
def __remotes_from_dir(dir):
- return os.listdir(os.path.join(basedir.get(), dir))
+ d = os.path.join(basedir.get(), dir)
+ if os.path.exists(d):
+ return os.listdir(d)
+ else:
+ return None
def remotes_list():
"""Return the list of remotes in the repository
for line in stream:
# Only consider Pull lines
m = re.match('^Pull: (.*)\n$', line)
- branches.append(refspec_localpart(m.group(1)))
+ if m:
+ branches.append(refspec_localpart(m.group(1)))
stream.close()
elif remote in __remotes_from_dir('branches'):
# old-style branches only declare one branch
if branchname in remotes_local_branches(remote):
return remote
- # FIXME: in the case of local branch we should maybe set remote to
- # "." but are we even sure it is the only case left ?
-
- # if we get here we've found nothing
+ # if we get here we've found nothing, the branch is a local one
return None
+
+def fetch_head():
+ """Return the git id for the tip of the parent branch as left by
+ 'git fetch'.
+ """
+
+ fetch_head=None
+ stream = open(os.path.join(basedir.get(), 'FETCH_HEAD'), "r")
+ for line in stream:
+ # Only consider lines not tagged not-for-merge
+ m = re.match('^([^\t]*)\t\t', line)
+ if m:
+ if fetch_head:
+ raise GitException, "StGit does not support multiple FETCH_HEAD"
+ else:
+ fetch_head=m.group(1)
+ stream.close()
+
+ # here we are sure to have a single fetch_head
+ return fetch_head
+
+def all_refs():
+ """Return a list of all refs in the current repository.
+ """
+
+ return [line.split()[1] for line in _output_lines(['git-show-ref'])]