X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/683840616efea425300859fe838add2b6b559a2c..9b4a6e70a1390d653201e48e12d09005b3fba6c8:/stgit/git.py diff --git a/stgit/git.py b/stgit/git.py index 27d2595..0955be7 100644 --- a/stgit/git.py +++ b/stgit/git.py @@ -43,7 +43,6 @@ class GRun(Run): """ Run.__init__(self, 'git', *cmd) - # # Classes # @@ -154,14 +153,12 @@ def get_commit(id_hash): def get_conflicts(): """Return the list of file conflicts """ - conflicts_file = os.path.join(basedir.get(), 'conflicts') - if os.path.isfile(conflicts_file): - f = file(conflicts_file) - names = [line.strip() for line in f.readlines()] - f.close() - return names - else: - return None + names = set() + for line in GRun('ls-files', '-z', '--unmerged' + ).raw_output().split('\0')[:-1]: + stat, path = line.split('\t', 1) + names.add(path) + return list(names) def exclude_files(): files = [os.path.join(basedir.get(), 'info', 'exclude')] @@ -170,7 +167,7 @@ def exclude_files(): files.append(user_exclude) return files -def ls_files(files, tree = None, full_name = True): +def ls_files(files, tree = 'HEAD', full_name = True): """Return the files known to GIT or raise an error otherwise. It also converts the file to the full path relative the the .git directory. """ @@ -185,11 +182,26 @@ def ls_files(files, tree = None, full_name = True): args.append('--') args.extend(files) try: - return GRun('ls-files', '--error-unmatch', *args).output_lines() + # use a set to avoid file names duplication due to different stages + fileset = set(GRun('ls-files', '--error-unmatch', *args).output_lines()) except GitRunException: # just hide the details of the 'git ls-files' command we use raise GitException, \ 'Some of the given paths are either missing or not known to GIT' + return list(fileset) + +def parse_git_ls(output): + t = None + for line in output.split('\0'): + if not line: + # There's a zero byte at the end of the output, which + # gives us an empty string as the last "line". + continue + if t == None: + mode_a, mode_b, sha1_a, sha1_b, t = line.split(' ') + else: + yield (t, line) + t = None def tree_status(files = None, tree_id = 'HEAD', unknown = False, noexclude = True, verbose = False, diff_flags = []): @@ -207,6 +219,8 @@ def tree_status(files = None, tree_id = 'HEAD', unknown = False, refresh_index() + if files is None: + files = [] cache_files = [] # unknown files @@ -226,31 +240,40 @@ def tree_status(files = None, tree_id = 'HEAD', unknown = False, # conflicted files conflicts = get_conflicts() - if not conflicts: - conflicts = [] cache_files += [('C', filename) for filename in conflicts if not files or filename in files] reported_files = set(conflicts) - - # files in the index - args = diff_flags + [tree_id] - if files: - args += ['--'] + files - for line in GRun('diff-index', *args).output_lines(): - fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) - if fs[1] not in reported_files: - cache_files.append(fs) - reported_files.add(fs[1]) - - # files in the index but changed on (or removed from) disk - args = list(diff_flags) - if files: - args += ['--'] + files - for line in GRun('diff-files', *args).output_lines(): - fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) - if fs[1] not in reported_files: - cache_files.append(fs) - reported_files.add(fs[1]) + files_left = [f for f in files if f not in reported_files] + + # files in the index. Only execute this code if no files were + # specified when calling the function (i.e. report all files) or + # files were specified but already found in the previous step + if not files or files_left: + args = diff_flags + [tree_id] + if files_left: + args += ['--'] + files_left + for t, fn in parse_git_ls(GRun('diff-index', '-z', *args).raw_output()): + # the condition is needed in case files is emtpy and + # diff-index lists those already reported + if not fn in reported_files: + cache_files.append((t, fn)) + reported_files.add(fn) + files_left = [f for f in files if f not in reported_files] + + # files in the index but changed on (or removed from) disk. Only + # execute this code if no files were specified when calling the + # function (i.e. report all files) or files were specified but + # already found in the previous step + if not files or files_left: + args = list(diff_flags) + if files_left: + args += ['--'] + files_left + for t, fn in parse_git_ls(GRun('diff-files', '-z', *args).raw_output()): + # the condition is needed in case files is empty and + # diff-files lists those already reported + if not fn in reported_files: + cache_files.append((t, fn)) + reported_files.add(fn) if verbose: out.done() @@ -446,109 +469,6 @@ def rename_branch(from_name, to_name): and os.path.exists(os.path.join(reflog_dir, from_name)): rename(reflog_dir, from_name, to_name) -def add(names): - """Add the files or recursively add the directory contents - """ - # generate the file list - files = [] - for i in names: - if not os.path.exists(i): - raise GitException, 'Unknown file or directory: %s' % i - - if os.path.isdir(i): - # recursive search. We only add files - for root, dirs, local_files in os.walk(i): - for name in [os.path.join(root, f) for f in local_files]: - if os.path.isfile(name): - files.append(os.path.normpath(name)) - elif os.path.isfile(i): - files.append(os.path.normpath(i)) - else: - raise GitException, '%s is not a file or directory' % i - - if files: - try: - GRun('update-index', '--add', '--').xargs(files) - except GitRunException: - 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 = GRun('ls-files', source).output_lines() - 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 force: - for f in files: - if os.path.exists(f): - raise GitException, '%s exists. Remove it first' %f - if files: - GRun('update-index', '--remove', '--').xargs(files) - else: - if files: - GRun('update-index', '--force-remove', '--').xargs(files) - # Persons caching __user = None __author = None @@ -695,83 +615,42 @@ def apply_diff(rev1, rev2, check_index = True, files = None): return True -def merge(base, head1, head2, recursive = False): +stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S) + +def merge_recursive(base, head1, head2): """Perform a 3-way merge between base, head1 and head2 into the local tree """ refresh_index() - - err_output = None - if recursive: - # this operation tracks renames but it is slower (used in - # general when pushing or picking patches) - try: - # discard output to mask the verbose prints of the tool - GRun('merge-recursive', base, '--', head1, head2 - ).discard_output() - except GitRunException, ex: - err_output = str(ex) - pass - else: - # the fast case where we don't track renames (used when the - # distance between base and heads is small, i.e. folding or - # synchronising patches) - try: - GRun('read-tree', '-u', '-m', '--aggressive', - base, head1, head2).run() - except GitRunException: - raise GitException, 'read-tree failed (local changes maybe?)' - - # check the index for unmerged entries - files = {} - stages_re = re.compile('^([0-7]+) ([0-9a-f]{40}) ([1-3])\t(.*)$', re.S) - - for line in GRun('ls-files', '--unmerged', '--stage', '-z' - ).raw_output().split('\0'): - if not line: - continue - - mode, hash, stage, path = stages_re.findall(line)[0] - - if not path in files: - files[path] = {} - files[path]['1'] = ('', '') - files[path]['2'] = ('', '') - files[path]['3'] = ('', '') - - 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: - # remove additional files that might be generated for some - # newer versions of GIT - for suffix in [base, head1, head2]: - if not suffix: - continue - fname = path + '~' + suffix - if os.path.exists(fname): - os.remove(fname) - - stages = files[path] - if gitmergeonefile.merge(stages['1'][1], stages['2'][1], - stages['3'][1], path, stages['1'][0], - stages['2'][0], stages['3'][0]) != 0: - errors = True - - if errors: - raise GitException, 'GIT index merging failed (possible conflicts)' - -def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = []): + p = GRun('merge-recursive', base, '--', head1, head2).env( + { 'GITHEAD_%s' % base: 'ancestor', + 'GITHEAD_%s' % head1: 'current', + 'GITHEAD_%s' % head2: 'patched'}).returns([0, 1]) + output = p.output_lines() + if p.exitcode: + # There were conflicts + conflicts = [l.strip() for l in output if l.startswith('CONFLICT')] + out.info(*conflicts) + + # try the interactive merge or stage checkout (if enabled) + for filename in get_conflicts(): + if (gitmergeonefile.merge(filename)): + # interactive merge succeeded + resolved([filename]) + + # any conflicts left unsolved? + cn = len(get_conflicts()) + if cn: + raise GitException, "%d conflict(s)" % cn + +def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = [], + binary = True): """Show the diff between rev1 and rev2 """ if not files: files = [] + if binary and '--binary' not in diff_flags: + diff_flags = diff_flags + ['--binary'] if rev1 and rev2: return GRun('diff-tree', '-p', @@ -787,12 +666,9 @@ def diff(files = None, rev1 = 'HEAD', rev2 = None, diff_flags = []): else: return '' -# TODO: take another parameter representing a diff string as we -# usually invoke git.diff() form the calling functions -def diffstat(files = None, rev1 = 'HEAD', rev2 = None): - """Return the diffstat between rev1 and rev2.""" - return GRun('apply', '--stat', '--summary' - ).raw_input(diff(files, rev1, rev2)).raw_output() +def diffstat(diff): + """Return the diffstat of the supplied diff.""" + return GRun('apply', '--stat', '--summary').raw_input(diff).raw_output() def files(rev1, rev2, diff_flags = []): """Return the files modified between rev1 and rev2 @@ -872,6 +748,17 @@ def reset(files = None, tree_id = None, check_out = True): if not files: __set_head(tree_id) +def resolved(filenames, reset = None): + if reset: + stage = {'ancestor': 1, 'current': 2, 'patched': 3}[reset] + GRun('checkout-index', '--no-create', '--stage=%d' % stage, + '--stdin', '-z').input_nulterm(filenames).no_output() + GRun('update-index', '--add', '--').xargs(filenames) + for filename in filenames: + gitmergeonefile.clean_up(filename) + # update the access and modificatied times + os.utime(filename, None) + def fetch(repository = 'origin', refspec = None): """Fetches changes from the remote repository, using 'git fetch' by default. @@ -966,7 +853,7 @@ def apply_patch(filename = None, diff = None, base = None, top = commit(message = 'temporary commit used for applying a patch', parents = [base]) switch(orig_head) - merge(base, orig_head, top) + merge_recursive(base, orig_head, top) def clone(repository, local_dir): """Clone a remote repository. At the moment, just use the @@ -992,7 +879,6 @@ def refspec_remotepart(refspec): return m.group(1) else: raise GitException, 'Cannot parse refspec "%s"' % line - def __remotes_from_config(): return config.sections_matching(r'remote\.(.*)\.url')