diff --git a/hgext/shelve.py b/hgext/shelve.py --- a/hgext/shelve.py +++ b/hgext/shelve.py @@ -432,6 +432,45 @@ cmdutil.exportfile(repo, [node], fp, opts=mdiff.diffopts(git=True), match=match) +def _encodemergerecords(records): + """Encode mergestate records to store in changeset extras. + Takes list of tuples as input and returns str. + """ + items = [ + '%s\033%s' % (rtype, record) + for rtype, record in sorted(records) + ] + return "\n".join(items) + +def _decodemergerecords(data): + """Decode mergestate record from changeset extras to return + a list of tuples. + """ + records = [] + for l in data.split('\n'): + rtype, record = l.split('\033') + records.append((rtype, record)) + return records + +def _storeunresolvedmerge(ui, repo, name=None, extra=None): + """Store the mergestate information in changeset extra + if config option `experimental.store-in-extra` is set True. + + Otherwise, move the usual mergestate information from + `.hg/merge` to `.hg/merge-unresolved/`. + + This will clear the mergestate and also stores the mergestate + information for later restoration. + """ + if ui.config('experimental', 'store-in-extra'): + ms = merge.mergestate.read(repo) + extra['mergerecords'] = _encodemergerecords(ms._readrecords()) + ms.reset() + else: + if not repo.vfs.exists('merge-unresolved'): + repo.vfs.mkdir('merge-unresolved') + repo.vfs.rename('merge', 'merge-unresolved/%s' % name) + def _includeunknownfiles(repo, pats, opts, extra): s = repo.status(match=scmutil.match(repo[None], pats, opts), unknown=True) @@ -439,6 +478,18 @@ extra['shelve_unknown'] = '\0'.join(s.unknown) repo[None].add(s.unknown) +def _isunresolvedshelve(ui, repo, sname=None, shelvectx=None): + """Checks whether the given shelve is unresolved or not""" + if shelvectx is not None: + return shelvectx.extra().get('unresolved-merge') + + if not ui.config('experimental', 'store-in-extra'): + return repo.vfs.exists('merge-unresolved/%s' % sname) + + #TODO: add functionality to distinguish unresolved shelves + # on doing `hg shelve --list` when `experimental.store-in-extra` + # set True + def _finishshelve(repo, tr): if phases.supportinternal(repo): tr.close() @@ -454,10 +505,19 @@ def _docreatecmd(ui, repo, pats, opts): wctx = repo[None] parents = wctx.parents() - if len(parents) > 1: - raise error.Abort(_('cannot shelve while merging')) + unresolved = opts.get('unresolved') parent = parents[0] origbranch = wctx.branch() + ms = merge.mergestate.read(repo) + if unresolved: + if not ms.active(): + raise error.Abort(_('no active mergestate found')) + elif not list(ms.unresolved()): + raise error.Abort(_('no unresolved ' + 'files found in the mergestate')) + elif ms.active(): + raise error.Abort(_('mergestate found\n' + 'try with --unresolved to shelve conflicts')) if parent.node() != nodemod.nullid: desc = "changes to: %s" % parent.description().split('\n', 1)[0] @@ -482,6 +542,9 @@ name = getshelvename(repo, parent, opts) activebookmark = _backupactivebookmark(repo) extra = {'internal': 'shelve'} + if unresolved: + extra['unresolved-merge'] = True + _storeunresolvedmerge(ui, repo, name, extra) if includeunknown: _includeunknownfiles(repo, pats, opts, extra) @@ -577,6 +640,7 @@ """subcommand that displays the list of shelves""" pats = set(pats) width = 80 + namewidth = 16 if not ui.plain(): width = ui.termwidth() namelabel = 'shelve.newest' @@ -585,13 +649,16 @@ sname = util.split(name)[1] if pats and sname not in pats: continue + if _isunresolvedshelve(ui, repo, sname): + sname += ' (unresolved)' + namewidth += len(' (unresolved)') ui.write(sname, label=namelabel) namelabel = 'shelve.name' if ui.quiet: ui.write('\n') continue - ui.write(' ' * (16 - len(sname))) - used = 16 + ui.write(' ' * (namewidth - len(sname))) + used = namewidth date = dateutil.makedate(mtime) age = '(%s)' % templatefilters.age(date, abbrev=True) ui.write(age, label='shelve.age') @@ -694,6 +761,26 @@ ui.status(_('marked working directory as branch %s\n') % branchtorestore) +def restoreunresolvedshelve(ui, repo, shelvectx, basename): + """Change the parents of the dirstate to the parents of the stored + unresolved shelvectx. + + Rewrite the mergestate from changeset extra if + `experimental.store-in-extra` is set True. + + Otherwise, replace `.hg/merge` with `.hg/merge-unresolved/` + to restore the status of the resolved files in the shelvectx. + """ + p1, p2 = shelvectx.parents() + repo.dirstate.setparents(p1.node(), p2.node()) + + if ui.config('experimental', 'store-in-extra'): + records = shelvectx.extra().get('mergerecords') + ms = merge.mergestate.clean(repo) + ms._writerecordsv2(_decodemergerecords(records)) + else: + repo.vfs.rename('merge-unresolved/%s' % basename, 'merge') + def unshelvecleanup(ui, repo, name, opts): """remove related files after an unshelve""" if not opts.get('keep'): @@ -898,7 +985,9 @@ _('restore shelved change with given name'), _('NAME')), ('t', 'tool', '', _('specify merge tool')), ('', 'date', '', - _('set date for temporary commits (DEPRECATED)'), _('DATE'))], + _('set date for temporary commits (DEPRECATED)'), _('DATE')), + ('', 'unresolved', None, + _('unshelve mergestate with unresolved files'))], _('hg unshelve [[-n] SHELVED]'), helpcategory=command.CATEGORY_WORKING_DIRECTORY) def unshelve(ui, repo, *shelved, **opts): @@ -944,6 +1033,7 @@ opts = pycompat.byteskwargs(opts) abortf = opts.get('abort') continuef = opts.get('continue') + unresolved = opts.get('unresolved') if not abortf and not continuef: cmdutil.checkunfinished(repo) shelved = list(shelved) @@ -999,6 +1089,8 @@ if not shelvedfile(repo, basename, patchextension).exists(): raise error.Abort(_("shelved change '%s' not found") % basename) + if unresolved: + cmdutil.bailifchanged(repo) repo = repo.unfiltered() lock = tr = None @@ -1019,18 +1111,35 @@ tmpwctx, addedbefore = _commitworkingcopychanges(ui, repo, opts, tmpwctx) repo, shelvectx = _unshelverestorecommit(ui, repo, tr, basename) + unresolvedshelve = _isunresolvedshelve(ui, repo, basename, shelvectx) _checkunshelveuntrackedproblems(ui, repo, shelvectx) branchtorestore = '' if shelvectx.branch() != shelvectx.p1().branch(): branchtorestore = shelvectx.branch() - shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, - basename, pctx, tmpwctx, - shelvectx, branchtorestore, - activebookmark) + if unresolved: + if not unresolvedshelve: + raise error.Abort(_('%s is not an unresolved shelve\n') % + basename) + p1, p2 = shelvectx.parents() + if pctx.node() not in [p1.node(), p2.node()]: + raise error.Abort(_('dirstate is not on either of the merge' + ' parents.\nuse hg update to one of the' + ' merge parents.')) + elif unresolvedshelve: + raise error.Abort(_('%s is an unresolved shelve, use' + ' --unresolved to unshelve it') % basename) + else: + shelvectx = _rebaserestoredcommit(ui, repo, opts, tr, oldtiprev, + basename, pctx, tmpwctx, + shelvectx, branchtorestore, + activebookmark) overrides = {('ui', 'forcemerge'): opts.get('tool', '')} with ui.configoverride(overrides, 'unshelve'): mergefiles(ui, repo, pctx, shelvectx) + if unresolved: + with repo.dirstate.parentchange(): + restoreunresolvedshelve(ui, repo, shelvectx, basename) restorebranch(ui, repo, branchtorestore) _forgetunknownfiles(repo, shelvectx, addedbefore) @@ -1068,7 +1177,9 @@ _('interactive mode, only works while creating a shelve')), ('', 'stat', None, _('output diffstat-style summary of changes (provide the names of ' - 'the shelved changes as positional arguments)') + 'the shelved changes as positional arguments)')), + ('', 'unresolved', None, + _('unshelve mergestate with unresolved files') )] + cmdutil.walkopts, _('hg shelve [OPTION]... [FILE]...'), helpcategory=command.CATEGORY_WORKING_DIRECTORY) diff --git a/mercurial/configitems.py b/mercurial/configitems.py --- a/mercurial/configitems.py +++ b/mercurial/configitems.py @@ -628,6 +628,9 @@ coreconfigitem('experimental', 'sparse-read.min-gap-size', default='65K', ) +coreconfigitem('experimental', 'store-in-extra', + default=False, +) coreconfigitem('experimental', 'treemanifest', default=False, ) diff --git a/tests/test-shelve-unresolved.t b/tests/test-shelve-unresolved.t new file mode 100644 --- /dev/null +++ b/tests/test-shelve-unresolved.t @@ -0,0 +1,538 @@ +#testcases extrastorage usualstorage + + $ addunresolvedmerge() { + > echo A >> $1 + > echo A >> $2 + > hg ci -m A + > echo B >> $1 + > echo B >> $2 + > hg ci -m B + > hg up $3 + > echo C >> $1 + > echo C >> $2 + > hg ci -m C + > hg merge -r $(($3+1)) + > } + +Test shelve with unresolved mergestate + + $ cat >> $HGRCPATH < [extensions] + > shelve = + > EOF + +#if extrastorage + + $ cat <> $HGRCPATH + > [experimental] + > store-in-extra = True + > EOF + +#endif + + $ hg init shelve-unresolved + $ cd shelve-unresolved + $ echo A >> file1 + $ echo A >> file2 + $ hg ci -Am A + adding file1 + adding file2 + $ echo foo >> bar + $ hg add bar + +-- should abort on absence of mergestate + $ hg shelve --unresolved + abort: no active mergestate found + [255] + + $ hg forget bar + $ echo B >> file1 + $ echo B >> file2 + $ hg ci -m B + $ hg up 0 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo C >> file1 + $ echo C >> file2 + $ hg ci -m C + created new head + $ hg merge + merging file1 + merging file2 + warning: conflicts while merging file1! (edit, then use 'hg resolve --mark') + warning: conflicts while merging file2! (edit, then use 'hg resolve --mark') + 0 files updated, 0 files merged, 0 files removed, 2 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + [1] + +-- let's partially solve the conflicts + $ cat > file1 < A + > B + > C + > EOF + $ hg resolve -m file1 + +-- mark file2 as resolved to check abort + $ hg resolve -m file2 + (no more unresolved files) + $ hg log -G + @ changeset: 2:69004294ad57 + | tag: tip + | parent: 0:c32ef6121744 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: C + | + | @ changeset: 1:fd9a4049234b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 0:c32ef6121744 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: A + + $ cat file1 + A + B + C + $ hg diff + diff -r 69004294ad57 file1 + --- a/file1 Thu Jan 01 00:00:00 1970 +0000 + +++ b/file1 Thu Jan 01 00:00:00 1970 +0000 + @@ -1,2 +1,3 @@ + A + +B + C + diff -r 69004294ad57 file2 + --- a/file2 Thu Jan 01 00:00:00 1970 +0000 + +++ b/file2 Thu Jan 01 00:00:00 1970 +0000 + @@ -1,2 +1,6 @@ + A + +<<<<<<< working copy: 69004294ad57 - test: C + C + +======= + +B + +>>>>>>> merge rev: fd9a4049234b - test: B + +-- should abort on absence of conflicts + $ hg shelve + abort: mergestate found + try with --unresolved to shelve conflicts + [255] + $ hg shelve --unresolved + abort: no unresolved files found in the mergestate + [255] + $ hg resolve -u file2 + $ hg resolve -l + R file1 + U file2 + +-- should suggest --unresolved on shelving with mergestate + $ hg shelve + abort: mergestate found + try with --unresolved to shelve conflicts + [255] + +#if extrastorage + $ hg shelve --unresolved + shelved as default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved +#else + $ hg shelve --unresolved + shelved as default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved +#endif + + $ cat file1 + A + C + $ hg log -G + @ changeset: 2:69004294ad57 + | tag: tip + | parent: 0:c32ef6121744 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: C + | + | o changeset: 1:fd9a4049234b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 0:c32ef6121744 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: A + +#if extrastorage + $ hg shelve --list + default (1s ago) changes to: C + $ hg shelve --patch + default (1s ago) changes to: C + + diff --git a/file1 b/file1 + --- a/file1 + +++ b/file1 + @@ -1,2 +1,3 @@ + A + +B + C + diff --git a/file2 b/file2 + --- a/file2 + +++ b/file2 + @@ -1,2 +1,6 @@ + A + +<<<<<<< working copy: 69004294ad57 - test: C + C + +======= + +B + +>>>>>>> merge rev: fd9a4049234b - test: B +else + $ hg shelve --list + default (1s ago) changes to: C + $ hg shelve --patch + default (1s ago) changes to: C + + diff --git a/file1 b/file1 + --- a/file1 + +++ b/file1 + @@ -1,2 +1,3 @@ + A + +B + C + diff --git a/file2 b/file2 + --- a/file2 + +++ b/file2 + @@ -1,2 +1,6 @@ + A + +<<<<<<< working copy: 69004294ad57 - test: C + C + +======= + +B + +>>>>>>> merge rev: fd9a4049234b - test: B +#endif + +-- now, fix an urgent bug + $ echo fixed >> bug + $ ls + bar + bug + file1 + file1.orig + file2 + file2.orig + $ hg add bug + $ hg ci -m "fix bug" + $ hg log -G + @ changeset: 3:a53a9a7475b3 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: fix bug + | + o changeset: 2:69004294ad57 + | parent: 0:c32ef6121744 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: C + | + | o changeset: 1:fd9a4049234b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 0:c32ef6121744 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: A + +-- let's get back to the old mergestate +-- we need to update to one of the merge parents. otherwise, abort. +#if extrastorage + $ hg unshelve --unresolved + unshelving change 'default' + abort: dirstate is not on either of the merge parents. + use hg update to one of the merge parents. + [255] +#else + $ hg unshelve --unresolved + unshelving change 'default' + abort: dirstate is not on either of the merge parents. + use hg update to one of the merge parents. + [255] +#endif + + $ hg up 2 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ cat file2 + A + C + +-- flag --unshelve is not passed. but, the last shelve is unresolved + $ hg unshelve + unshelving change 'default' + abort: default is an unresolved shelve, use --unresolved to unshelve it + [255] + +#if extrastorage + $ hg unshelve --unresolved + unshelving change 'default' +#else + $ hg unshelve --unresolved + unshelving change 'default' +#endif + + $ hg log -G + o changeset: 3:a53a9a7475b3 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: fix bug + | + @ changeset: 2:69004294ad57 + | parent: 0:c32ef6121744 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: C + | + | @ changeset: 1:fd9a4049234b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 0:c32ef6121744 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: A + + $ cat file1 + A + B + C + $ hg diff + diff -r 69004294ad57 file1 + --- a/file1 Thu Jan 01 00:00:00 1970 +0000 + +++ b/file1 Thu Jan 01 00:00:00 1970 +0000 + @@ -1,2 +1,3 @@ + A + +B + C + diff -r 69004294ad57 file2 + --- a/file2 Thu Jan 01 00:00:00 1970 +0000 + +++ b/file2 Thu Jan 01 00:00:00 1970 +0000 + @@ -1,2 +1,6 @@ + A + +<<<<<<< working copy: 69004294ad57 - test: C + C + +======= + +B + +>>>>>>> merge rev: fd9a4049234b - test: B + $ cat file2 + A + <<<<<<< working copy: 69004294ad57 - test: C + C + ======= + B + >>>>>>> merge rev: fd9a4049234b - test: B + $ hg resolve -l + R file1 + U file2 + +-- flag --unresolved is passed but the top most shelve is not unresolved +#if extrastorage + $ hg shelve --unresolved + shelved as default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved +#else + $ hg shelve --unresolved + shelved as default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved +#endif + + $ echo garbage >> bug + $ hg st + ? bar + ? bug + ? file1.orig + ? file2.orig + $ hg add bug + $ hg shelve + shelved as default-01 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg unshelve --unresolved + unshelving change 'default-01' + abort: default-01 is not an unresolved shelve + + [255] + +-- now, unshelve default +#if extrastorage + $ hg unshelve -n default --unresolved +#else + $ hg unshelve -n default --unresolved +#endif + +-- commit the merge after completing conflict resolution + $ cat >> file2 < A + > B + > C + > EOF + $ hg resolve -m file2 + (no more unresolved files) + $ hg ci -m merge + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + checked 5 changesets with 9 changes to 3 files + $ hg log -G + @ changeset: 4:745dca2ee1f1 + |\ tag: tip + | | parent: 2:69004294ad57 + | | parent: 1:fd9a4049234b + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: merge + | | + | | o changeset: 3:a53a9a7475b3 + | |/ user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: fix bug + | | + | o changeset: 2:69004294ad57 + | | parent: 0:c32ef6121744 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: C + | | + o | changeset: 1:fd9a4049234b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 0:c32ef6121744 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: A + + +-- flag --unsresolved is passed but we don’t have any unresolved shelve + $ hg unshelve --unresolved + unshelving change 'default-01' + abort: default-01 is not an unresolved shelve + + [255] + +-- when working directory is dirty + $ addunresolvedmerge file1 file2 5 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + created new head + merging file1 + merging file2 + warning: conflicts while merging file1! (edit, then use 'hg resolve --mark') + warning: conflicts while merging file2! (edit, then use 'hg resolve --mark') + 0 files updated, 0 files merged, 0 files removed, 2 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon + [1] + +#if extrastorage + $ hg shelve --unresolved + shelved as default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved +#else + $ hg shelve --unresolved + shelved as default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved +#endif + + $ echo dirt >> bar + $ hg add bar + $ hg unshelve --unresolved + unshelving change 'default' + abort: uncommitted changes + [255] + +--- unshelve --unresolved when there is another merge going on + $ hg ci -m dirt + $ hg up 7 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo foo >> bug + $ hg add bug + $ hg ci -m foo2 + created new head + $ hg log -G + @ changeset: 9:c74a624102ed + | tag: tip + | parent: 7:974ec4298b79 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: foo2 + | + | o changeset: 8:4acf09fb3a59 + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: dirt + | + o changeset: 7:974ec4298b79 + | parent: 5:db68c6c84fe6 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: C + | + | o changeset: 6:e236d497f76b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 5:db68c6c84fe6 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: A + | + o changeset: 4:745dca2ee1f1 + |\ parent: 2:69004294ad57 + | | parent: 1:fd9a4049234b + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: merge + | | + | | o changeset: 3:a53a9a7475b3 + | |/ user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: fix bug + | | + | o changeset: 2:69004294ad57 + | | parent: 0:c32ef6121744 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: C + | | + o | changeset: 1:fd9a4049234b + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: B + | + o changeset: 0:c32ef6121744 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: A + + $ hg merge -r 8 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + +#if extrastorage + $ hg unshelve --unresolved + unshelving change 'default' + abort: outstanding uncommitted merge + [255] +#else + $ hg unshelve --unresolved + unshelving change 'default' + abort: outstanding uncommitted merge + [255] +#endif diff --git a/tests/test-shelve.t b/tests/test-shelve.t --- a/tests/test-shelve.t +++ b/tests/test-shelve.t @@ -85,6 +85,7 @@ --stat output diffstat-style summary of changes (provide the names of the shelved changes as positional arguments) + --unresolved unshelve mergestate with unresolved files -I --include PATTERN [+] include names matching the given patterns -X --exclude PATTERN [+] exclude names matching the given patterns --mq operate on patch repository