From: Catalin Marinas Date: Tue, 3 Jun 2008 21:22:45 +0000 (+0100) Subject: Merge branch 'stable' X-Git-Tag: v0.15-rc1~218 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/commitdiff_plain/3e022c8a41837e0894d017c4912b77e4b1792744?hp=-c Merge branch 'stable' --- 3e022c8a41837e0894d017c4912b77e4b1792744 diff --combined stgit/git.py index 570003c,8e6bdf4..0955be7 --- a/stgit/git.py +++ b/stgit/git.py @@@ -43,6 -43,7 +43,6 @@@ class GRun(Run) """ Run.__init__(self, 'git', *cmd) - # # Classes # @@@ -153,12 -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')] @@@ -182,14 -185,25 +182,27 @@@ def ls_files(files, tree = 'HEAD', full 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 = []): """Get the status of all changed files, or of a selected set of @@@ -227,6 -241,8 +240,6 @@@ # 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) @@@ -239,13 -255,12 +252,12 @@@ args = diff_flags + [tree_id] if files_left: args += ['--'] + files_left - for line in GRun('diff-index', *args).output_lines(): - fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) + 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 fs[1] not in reported_files: - cache_files.append(fs) - reported_files.add(fs[1]) + 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 @@@ -256,13 -271,12 +268,12 @@@ args = list(diff_flags) if files_left: args += ['--'] + files_left - for line in GRun('diff-files', *args).output_lines(): - fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) + 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 fs[1] not in reported_files: - cache_files.append(fs) - reported_files.add(fs[1]) + if not fn in reported_files: + cache_files.append((t, fn)) + reported_files.add(fn) if verbose: out.done() @@@ -458,6 -472,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 @@@ -604,33 -721,77 +615,33 @@@ def apply_diff(rev1, rev2, check_index 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)' + 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): @@@ -655,9 -816,12 +666,9 @@@ 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 @@@ -737,17 -901,6 +748,17 @@@ def reset(files = None, tree_id = None 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. @@@ -842,7 -995,7 +853,7 @@@ def apply_patch(filename = None, diff 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