[RFC] Amend commit messages

Laurens Holst laurens.nospam at grauw.nl
Wed Feb 23 05:51:53 CST 2011


I don’t think it’s necessary, or desirable.

It doesn’t happen that often, and when it does, usually you still have 
the opportunity to do hg rollback or even to edit it with MQ or histedit 
as you said.

If you’ve pushed out the changes already, then zut, so be it. Honestly, 
it’s really not a big deal to have a less-than-perfect commit message. 
Can’t change commit messages at all with SVN once you’ve committed, I 
think with the existing possibilities Mercurial is already a lot more 
powerful than that.

Taking it further seems to me like taking it too far, and hurting the 
immutable history principle. In fact, the ability to edit history 
already leads me to the tendency spend a lot of time on tidying it up 
that I really shouldn’t be spending, and is also actually pretty 
dangerous, you can lose your changes with relative ease. (Hopefully with 
the dead branches functionality it’ll be less dangerous.)

~Laurens

Op 23-2-2011 9:41, Gilles Moris schreef:
> Hello,
>
> I don't remember any extension in this area, but this is a recurrent question
> on the mailing list:
> "I made a mistake in my commit message. How can I change it?"
>
> We can redirect them to either mq or histedit, but usually we can only answer
> that this is not possible if the commit is already published.
> However, this question is legitimate. Whatever the number of reviews you can
> do, there always be some typo that can escape, like switched digits in a bug
> number or similar issues. The idea to have a message forever wrong is quite
> frustrating.
>
> 1. Goal
> The goal is to overlay another commit description to replace a previous commit
> message. The current history (committed SHA1) is not changed and those commit
> message amendments appear as additional commits.
>
> 2. Design
> The idea to wrap the changectx.description() method and replace the original
> commit message with something else. The design is somewhat borrowed to tags:
> get the new description from another file; if multiple heads exist for this
> file, take the latest filelog revision of that file by convention.
>
> Now see how the something else can structured:
>
> - as a single ".hgmessage" file located in the working directory, much like
> .hgtags". However this is more complex to handle than tags, as commit message
> can be multi-lines. This would require some markup language to delineate the
> mapping between the commit identifier and the message content. This would be
> specifically a problem during merges of this file, which would require knowledge
> of the format and increase the risk of incorrectly merge this file.
>
> - the second solution is to create a ".hgmessage" directory this time, and have
> file for each commit amended. The file name is the full hex SHA1 identifier of
> the amended commit and the content is just the new commit message. This lowers
> the risk of merge, and in case of merge it is quite straightforward to
> understand how to merge. However, this risk is not null. The problem here is
> that the commit message description history is intermangled with the regular
> code history. If the commit amendments are made in 2 different branches that
> are not supposed to be merged from a code standpoint, we're stuck with also
> 2 divergent commit descriptions.
>
> - so I came up with a third solution which is to put the commit messages in
> their owned named branch "hgmessage". That way the amendment history is
> decorrelated from the code history. The need to amend a commit message should
> be sufficiently rare that the overhead of a separate branch is acceptable.
>
> 3. Implementation
> As an roughly implemented extension inlined below.
>
> 4. Open issues
> - The name of the extension is message. The name of the command is "amend". I
> started with "message", then "editmessage". I like "amend" as it shows I do not
> edit the history.
> - More generally naming and wording.
> - Deleting one of the commit amendment files does not cancel the message
> edition as I am looking at the latest filelog revision, not at the manifest.
> Currently, you cancel a message amendment by creating a new amendment with the
> original content. That should be acceptable.
> - Performance: I did not find how to walk a specific directory of the store at
> start up, so I am walking the entire store (code taken from the cifiles
> extension). This also why I kept a directory even though I have a separate
> named branch: to provide further optimization of the start up time. Also there
> is no caching. Is some needed ?
> - Next steps: what do we do with this? I will wait for your comments to see if
> I am heading in the right direction, and probably then post the extension on
> Google Code or Bitbuckets. But then, does it deserve to be considered for
> inclusion as a bundled extension or a core patch?
>
> Regards.
> Gilles.
>
>
>
> import os, re
> from mercurial import hg, extensions, context, node, util, commands, cmdutil
> from mercurial import match as matchmod
> from mercurial.i18n import _
>
> def getmsgfolder(ui):
>      return ui.config('editmessage', 'folder', 'hgmessage')
>
> def getmsgbranch(ui):
>      return ui.config('editmessage', 'branch', 'hgmessage')
>
> def msgread(filelog, node):
>      return filelog.read(node).splitlines()
>
> def amenddesc(orig, ctx):
>      """wraps the changectx.description() method"""
>      repo = ctx._repo
>      nid = node.hex(ctx.node())
>
>      if nid in repo.amendedmsg:
>          fl = repo.file(getmsgfolder(repo.ui) + '/' + nid)
>          heads = [fl.rev(h) for h in fl.heads()]
>
>          if repo.ui.debugflag:
>              lines = []
>              for r in range(len(fl)-1, -1, -1):
>                  amctx = repo[fl.linkrev(r)]
>                  lines.append(_("amendment version: %d (%s)") %
>                               (r + 1, node.hex(amctx.node())))
>                  if len(heads)>  1 and r in heads:
>                      ln = _("WARNING: message amendment conflicts in rev %s") % \
>                           ', '.join(str(fl.linkrev(h)) for h in heads if h != r)
>                      lines.append(ln)
>                  lines.append(_("amended by: %s") % amctx.user())
>                  lines.append(_("amended on: %s") % util.datestr(amctx.date()))
>                  lines.append("")
>                  lines.extend(msgread(fl, fl.node(r)))
>                  lines.append("")
>                  lines.append("")
>              lines.append(_("original description:"))
>              lines.append("")
>              lines.extend(orig(ctx).splitlines())
>          elif repo.ui.verbose:
>              lines = msgread(fl, fl.tip())
>              amctx = repo[fl.linkrev(len(fl)-1)]
>              lines.insert(1, _("(amended by %s on %s)") % (
>                  amctx.user(),
>                  util.datestr(amctx.date())))
>              if len(heads)>  1:
>                  lines.insert(1,
>                               _("WARNING: message amendment conflicts in rev %s") %
>                               ", ".join(str(fl.linkrev(h)) for h in heads))
>          else:
>              lines = msgread(fl, fl.tip())
>
>          msg = '\n'.join(lines)
>      else:
>          msg = orig(ctx)
>      return msg
>
> def uisetup(ui):
>      extensions.wrapfunction(context.changectx, 'description', amenddesc)
>
> def reposetup(ui, repo):
>      repo.amendedmsg = set()
>
>      prefix = "data/%s/" % getmsgfolder(ui)
>      suffix = ".i"
>      plen = len(prefix)
>      slen = len(suffix)
>      lock = repo.lock()
>      try:
>          # this part can probably be greatly optimized by walking only one folder
>          # but using which API
>          for fn, efn, sz in repo.store.datafiles():
>              if fn[-slen:] == suffix and fn[:plen] == prefix:
>                  repo.amendedmsg.add(fn[plen:-slen])
>      finally:
>          lock.release()
>
> def editmsg(ui, repo, n, nidfn):
>      """creates the text template of the commit message to be edited"""
>      ctx = repo[n]
>      # use changelog for original message to bypass the
>      # overloaded ctx.description()
>      origdesc = repo.changelog.read(n)[4]
>      edittext = []
>
>      # user edits the last commit message available
>      if node.hex(n) in repo.amendedmsg:
>          fl = repo.file(nidfn)
>          edittext.extend(msgread(fl, fl.tip()))
>      else:
>          edittext.extend(origdesc.splitlines())
>      edittext.append("")
>
>      # standard HG comments when editing commit messages.
>      edittext.append(_("HG: Enter commit message."
>                        "  Lines beginning with 'HG:' are removed."))
>      edittext.append(_("HG: Leave message as is to abort edit."))
>      edittext.append("HG: --")
>      edittext.append(_("HG: user: %s") % ctx.user())
>      if ctx.p2():
>          edittext.append(_("HG: branch merge"))
>      if ctx.branch():
>          edittext.append(_("HG: branch '%s'") % ctx.branch())
>      edittext.extend([_("HG: files %s") % f for f in ctx.files()])
>
>      # Display all the previous version of the message for reference
>      edittext.append("HG:" + '=' * 35 + "8<" + '-' * 35)
>      if node.hex(n) in repo.amendedmsg:
>          for r in range(len(fl)-1, -1, -1):
>              amctx = repo[fl.linkrev(r)]
>              edittext.append("HG: amendment version: %d (%s)" %
>                              (r + 1, node.hex(amctx.node())))
>              edittext.append("HG: amended by: %s" % amctx.user())
>              edittext.append("HG: amended on: %s" % util.datestr(amctx.date()))
>              edittext.append("HG:")
>              edittext.extend("HG: " + l for l in msgread(fl, fl.node(r)))
>              edittext.append("HG:")
>              edittext.append("HG:" + '=' * 35 + "8<" + '-' * 35)
>      edittext.append("HG: original description:")
>      edittext.append("HG:")
>      origdesc = repo.changelog.read(n)[4]
>      edittext.extend(map(lambda l: "HG: " + l, origdesc.splitlines()))
>      edittext.append("")
>      return '\n'.join(edittext)
>
> def amend(ui, repo, rev, **opts):
>      """amend the commit message of the given revision
>
>      Change a previous commit message. The commit is given as the only argument
>      of the command. The history is not really edited. Another commit is created
>      by this command and will overlay the previous commit message. This command
>      can be run multiple times: the log will show the latest message.
>
>      Without -m or -l options, an editor will show up with the commit message
>      edition history as a reference.
>
>      If divergent amendment already exists for this commit, the command will
>      refuse to work. You will have first to merge the multiple heads on the named
>      branch (by default "hgmessage") used to handle the message amendment. The -f
>      option enables to override this behavior.
>
>      Only the commit message is amended. The -u and -d options allow only to
>      change the user and date of the amendement commit.
>
>      Returns 0 on success, 1 if nothing changed.
>      """
>      cmdutil.bail_if_changed(repo)
>
>      n = repo.lookup(rev)
>      msgbranch = getmsgbranch(ui)
>      hgmsgdir = getmsgfolder(ui)
>      fl = repo.file(hgmsgdir + '/' + node.hex(n))
>      if not opts.get('force') and len(fl.heads())>  1:
>          raise util.Abort("multiple heads for %s: merge %s branch or use -f" %
>                           (node.short(n), msgbranch))
>      savectx = os.getcwd(), repo.dirstate.parents()
>
>      os.chdir(repo.root)
>      # Create or check out the hgmessage branch
>      if msgbranch not in repo.branchtags():
>          hg.clean(repo, node.nullid, False)
>          repo.dirstate.setbranch(msgbranch)
>      else:
>          hg.clean(repo, msgbranch, False)
>
>      if not os.path.lexists(repo.wjoin(hgmsgdir)):
>          os.mkdir(repo.wjoin(hgmsgdir))
>      nidfn = os.path.join(hgmsgdir, node.hex(n))
>
>      text = cmdutil.logmessage(opts)
>      if not text:
>          temp = editmsg(ui, repo, n, nidfn)
>          text = ui.edit(temp, repo[n].user())
>          if text == temp:
>              # nothing changed
>              text = ""
>          else:
>              text = re.sub("(?m)^HG:.*(\n|$)", "", text)
>
>      if not text:
>          ui.status(_("nothing changed\n"))
>          return 1
>
>      text = text.strip() + '\n'
>      f = open(repo.wjoin(nidfn), 'w')
>      f.write(text)
>      f.close()
>      wctx = repo[None]
>      wctx.status(ignored=True, unknown=True)
>      if nidfn in wctx.ignored() or nidfn in wctx.unknown():
>          wctx.add([nidfn])
>      match = matchmod.exact(repo.root, '', [nidfn])
>      date = opts.get('date')
>      if date:
>          date = util.parsedate(date)
>      cimsg = "amend commit message of %s" % node.hex(n)
>      repo.commit(cimsg, opts.get('user'), date, match)
>
>      # restores the saved context
>      hg.clean(repo, savectx[1][0], False)
>      os.chdir(savectx[0])
>      return 0
>
> cmdtable = {
> 'amend':
>          (amend,
>           [('f', 'force', None, _('force edit message')),
>            ] + commands.commitopts + commands.commitopts2,
>          _('hg editmessage [OPTION] REV')),
> }
>
>
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at selenic.com
> http://selenic.com/mailman/listinfo/mercurial-devel
>


-- 
~~ Ushiko-san! Kimi wa doushite, Ushiko-san nan da!! ~~
Laurens Holst, developer, Utrecht, the Netherlands
Website: www.grauw.nl. Backbase employee; www.backbase.com


-------------- next part --------------
A non-text attachment was scrubbed...
Name: smime.p7s
Type: application/pkcs7-signature
Size: 6034 bytes
Desc: S/MIME Cryptographic Signature
URL: <http://selenic.com/pipermail/mercurial-devel/attachments/20110223/ccc1de33/attachment.bin>


More information about the Mercurial-devel mailing list