From: Catalin Marinas Date: Tue, 27 May 2008 22:18:22 +0000 (+0100) Subject: Merge branch 'stable' X-Git-Tag: v0.15-rc1~225 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/commitdiff_plain/78e5af69d99659769b052e4c00fe5b23a8356231?hp=-c Merge branch 'stable' Conflicts: t/t2700-refresh.sh --- 78e5af69d99659769b052e4c00fe5b23a8356231 diff --combined stgit/git.py index 4dc4dcf,6140fd9..570003c --- 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')] @@@ -167,7 -170,7 +167,7 @@@ 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. """ @@@ -182,13 -185,11 +182,13 @@@ 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 tree_status(files = None, tree_id = 'HEAD', unknown = False, noexclude = True, verbose = False, diff_flags = []): @@@ -206,6 -207,8 +206,8 @@@ refresh_index() + if files is None: + files = [] cache_files = [] # unknown files @@@ -225,29 -228,44 +227,42 @@@ # 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 line in GRun('diff-index', *args).output_lines(): + fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) + # 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]) + 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 line in GRun('diff-files', *args).output_lines(): + fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1)) + # 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 verbose: out.done() @@@ -443,6 -461,109 +458,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 @@@ -589,33 -710,77 +604,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): @@@ -640,9 -805,12 +655,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 @@@ -722,17 -890,6 +737,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. @@@ -827,7 -984,7 +842,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 @@@ -853,7 -1010,6 +868,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') diff --combined t/t2700-refresh.sh index 3759d0e,ffac295..aad6d45 --- a/t/t2700-refresh.sh +++ b/t/t2700-refresh.sh @@@ -6,10 -6,8 +6,10 @@@ test_description='Run "stg refresh" test_expect_success 'Initialize StGit stack' ' stg init && - echo expected.txt >> .git/info/exclude && + echo expected*.txt >> .git/info/exclude && echo patches.txt >> .git/info/exclude && + echo show.txt >> .git/info/exclude && + echo diff.txt >> .git/info/exclude && stg new p0 -m "base" && for i in 1 2 3; do echo base >> foo$i.txt && @@@ -33,7 -31,7 +33,7 @@@ test_expect_success 'Refresh top patch stg status && test -z "$(stg status)" && stg patches foo3.txt > patches.txt && - diff -u expected.txt patches.txt + test_cmp expected.txt patches.txt ' cat > expected.txt < patches.txt && - diff -u expected.txt patches.txt + test_cmp expected.txt patches.txt ' cat > expected.txt < patches.txt && - diff -u expected.txt patches.txt + test_cmp expected.txt patches.txt +' + +cat > expected.txt < expected2.txt < expected3.txt <> foo1.txt && + git add foo1.txt && + echo blah 1 >> foo1.txt && + echo baz 2 >> foo2.txt && + stg refresh --index && + stg patches foo1.txt > patches.txt && + git diff HEAD^..HEAD > show.txt && + stg diff > diff.txt && + test_cmp expected.txt patches.txt && + test_cmp expected2.txt show.txt && + test_cmp expected3.txt diff.txt && + stg new p5 -m "cleanup again" && + stg refresh ' + + test_expect_success 'Refresh moved files' ' + git mv foo1.txt foo1-new.txt && + stg refresh + ' + test_done