1. 42
  1.  

  2. 18

    Because there is nothing I love more than comparing Git and Mercurial using specific examples, here’s a great big piece of text discussing the design choices that went into these Git commands, and their Mercurial counterparts. Enjoy ;-)

    For those who don’t want to read the whole thing, the paragraphs on “See which branches you recently worked on” and “Show changed words instead of lines” probably have the most interesting tidbits.

    Leaderboards

    Enable the bundled churn extension, and use hg churn, with --changesets to count commits instead of lines touched; -t '{author|email}' to group changesets by the output of a template expression, in this case the committer’s e-mail address; and -r to limit to a subset of revisions.

    # hgrc:
    #   [extensions]
    #   churn =
    hg churn --changesets -t '{author|email}' -r 'date("2016-01-01 to 2016-12-01")'
    
    # git shortlog -sn --since='10 weeks' --until='2 weeks'
    

    Something I like about Git’s design: that you can type e.g. 2 weeks and it will interpret it as the date 2 weeks ago. Mercurial has date(-10) for ‘the date range from 10 days ago to now`, but it’s not the same …. Something I like about Mercurial’s design: that grouping and tallying changes has a separate command, rather than being a ‘summary’ option on top of a ‘log’ variant that groups commits (but is called ‘shortlog’ rather than ‘grouplog’)

    Praise instead of blame

    Defining an alias so you can praise instead of blame: I very much agree, and have indeed got a similar alias in my .hgrc. (In fact, I could have sworn git praise is defined out-of-the-box. Seems it isn’t. Was that SVN, then?)

    Something I like about Git’s design: that it lets you set options from the command line. Something I like about SVN’s design: that it includes praise by default. I’m not sure if it’s ironic or appropriate that the distributed VCSes are less sociable, here. :-)

    Hide whitespace noise / Show changed words instead of lines

    Whitespace: hg diff has a similar set of options.

    -w --ignore-all-space    ignore white space when comparing lines
    -b --ignore-space-change ignore changes in the amount of white space
    -B --ignore-blank-lines  ignore changes whose lines are all blank
    

    Mercurial doesn’t have --word-diff, though. It looks really nice, and I will definitely break out Git (or vimdiff) next time I could use this. Similarly nice: Github’s display, a line-wise diff with word-wise emphasis highlighting.

    See which branches you recently worked on

    Getting the ten branch heads with the most recent commit date is a job for revsets, of course; printing each one in a special way is a job for output templates.

    hg log -r 'limit(sort(head(), "-date"), 10)' -T '{rev} {bookmarks}\n'
    
    # git for-each-ref --count=10 --sort=-committerdate refs/heads/ --format="%(refname:short)"
    

    If you want the output to show commit IDs, dates, and branch names, add -T '{rev} ({date|isodate}): {bookmarks}\n'

    Something I like about Mercurial’s design: the clear separation of commit selection (the -r flag plus the revset DLS) and display (the -T flag plus the template DSL). Something I like about Git’s design: this Git command has placeholders with fully spelled-out names. Nice! Somebody should totally port that into git log’s --pretty format DSL.

    Also, you know how Mercurial users like to bring up consistency? This is a nice example of the importance of good names and consistent flags.

    • We have seen 3 log variants so far: log, shortlog, and for-each-ref.
    • The two commands with ‘log’ in the name are git log and git shortlog.
    • The two commands with log-like usage are git log and git for-each-ref.
    • The remaining command, git shortlog, has ‘log’ in its name, but is more interesting as a group-and-tally command: it either prepares changelogs (as its manpage suggests) or tallies churn (as seen in the article).
    • All three commands have different commit selection flags
    • All three commands have different formatting DSLs
    See what everyone’s been getting up to

    hg log + revsets again. And again we need only two flags: -r for commit selection, -T for display formatting.

    hg log -r 'date(-14) - merge()' -T '{rev} {desc|firstline}\n'
    
    # git log --all --since='2 weeks' --oneline --no-merges
    

    This is the sort of alias where we start to see bits (a revset, a template, even a template keyword) that we might want to reuse in other places, too. Let’s do that for a lark.

    # hgrc:
    #   [revsetalias]
    #   recent(datespec) = date(datespec) and not merge()
    #
    #   [templatealias]
    #   summary = '{desc|firstline}'
    #
    #   [templates]
    #   oneline = '{rev} {summary}\n'
    #
    #   [aliases]
    #   overview = log -r 'recent(-14)' -T oneline
    
    hg overview
    
    # git recent
    

    Admirably, Git ships with a one-line template by default. Mercurial doesn’t, though it is easy enough to define one. Admirably, hg log does not hide heads by default. Git’s log does hide heads (and I’ve seen it disorient many people), though it is easy enough to add --all.

    Remind yourself what you’ve been up to

    This is where we get to reuse the revsets and templates we defined in the previous section.

    # hrgrc:
    #   [aliases]
    #   recap = log -r 'recent(today) and user(<your e-mail>)' -T oneline
    
    hg recap
    
    Check which changes you’re about to pull / Review what you’re about to push

    Incoming! Does not require a fetch, either. Equally nifty: its brother, hg outgoing. When pushing/pulling, you can use -r to limit what you would push/pull to the ancestors of certain commits or bookmarks; as a result, hg incoming -r somebookmark works, too.

    hg incoming -T oneline --graph
    
    # git log --oneline --no-merges HEAD..<remote>/<branch>
    

    Something I like about Git’s design: namespaced tracking of last-known remote branch pointer positions was there from the start. I should check out Mercurial’s remotenames extension, though.

    Something I like about Mercurial’s design: it has first-class commands for a first-class mental concept. Providing hg incoming and hg outgoing is also very helpful to newbies: pushing and pulling is less scary if you can see for yourself what is about to happen.

    Generate a changelog

    You can probably guess what is coming: hg log -r ... -T ....

    hg log -r '<last tag>::master and not merge()' -T changelog
    
    # git log --oneline --no-merges <last tag>..HEAD
    

    Fancy-schmancy, automatically finding the most recent tag, and ordering by topological branch:

    hg log -r 'sort(
        last(sort(
            tag() and ancestors(master),
            "date"
        ))::. and not merge(),
        "topo"
    )'
    

    Qua comparing Git and Mercurial, there’s nothing in here we haven’t gone over yet. And whatever your VCS, the most important point isn’t even the specific command you need to use – it’s that you should identify the mental concepts you repeatedly use, and throw them in an alias.

    (Come to think of it, every system that has power users – every system that rewards learning – all these systems, be they shells, or TeX; Excel, Vim, Mercurial, or Git – all these systems have aliases or macros, and every power user I know started out by writing macros to make their own lives easier. That, for many, is the first bite of the apple. And the systems reward them richly for it.)

    1. 2

      This is an awesome post. Please submit it as a separate post or turn it into a blog post and link here?

      1. 2

        This post will make @jordigh proud when he sees it.

        1. 5

          Can confirm, am proud.

          1. 1

            :-)))

      2. 8
        • git push --force-with-lease :: probably my favorite example of git doing the “right” thing the wrong way.
        • git reflog :: for sleuthing after a horrifying rebase mistake
        • M-x magit-status (or equivalent staging tools like git add -p) :: really make things feel comfortable.
        • cp -a :: I often forget these are just files. Don’t forget! If your repo is in a delicate state, just copy the whole damned thing, and try it out.
        1. 1

          probably my favorite example of git doing the “right” thing the wrong way.

          elaborate why?

        2. 6

          One of my favorites that I cooked up:

          [alias]
            clogp = -c core.pager='less -p^commit.*$' log -p
          

          It’s “just” git log -p, but with your default pager overridden to prime less to search for the “commit” label, so you can just hit n to jump through your history commit by commit.

          1. 4

            Instead of using various formats for git log, I just install tig. It’s an easy way to pull up and browse recent activity, see what you’re about to push, etc.

            Another tip: use -p when adding code that you plan to commit (i.e. git add -p). It will show every hunk of the patch you’re about to add and give you a prompt where you can say ‘y’ or ‘n’ to include it. It basically forces you to do a quick code review (or at least sanity check) prior to committing changes. I’ve caught many a quick & dirty printf this way. It’s also useful for splitting unrelated changes into two distinct commits.

            1. 3

              I found this all on the internet, but don’t really remember where.

              dotfile

              # git lg
              git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
              # git ll
              git config --global alias.ll "log --pretty=format:'%C(yellow)%h%Cred%d\\ %Creset%s%Cblue\\ [%cn]' --decorate --numstat"
              
              # Better diffs (comes from git contrib scripts)
              git config --global pager.log 'diff-highlight | less'
              git config --global pager.show 'diff-highlight | less'
              git config --global pager.diff 'diff-highlight | less'
              git config --global interactive.diffFilter diff-highlight
              curl https://raw.githubusercontent.com/git/git/master/contrib/diff-highlight/diff-highlight > ~/bin/diff-highlight && chmod +x ~/bin/diff-highlight
              
              1. 3

                Added images for the git lg and git ll output.

              2. 2

                I stole this alias off of another article posted on lobste.rs:

                ll = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %Creset'
                
                1. 1

                  Share your favorite git tips/aliases if you’ve got some good ones!

                  1. 4

                    This is a Mercurial trick, but I use it on Git repos via hg-git, so it sorta counts? Maybe? ;-)

                    # hgrc:
                    #   [revsetalias]
                    #   feature($1) = _firstancestors($1) and not _firstancestors(master)
                    

                    This shows all commits that were made on a feature branch (branch in the Git sense), whethere they’ve been merged into master or not; and it ignores all commits that were made on the master branch, whether they’ve been merged into feature or not. Like this: only the commits marked ‘F’ for feature get shown.

                    hg log -r 'feature(myfeature)'
                    
                                   v master
                    o--o--o--o-----o
                        \     \   /
                         \     \ /
                          F--F--F
                                ^ myfeature
                    

                    I prefer rebase-based merging; but git-flow uses explicit merges, and this is really useful for examining that sort of history. It is possible because for each merge commit, Mercurial records which parent was the recipient (HEAD) and which parent was merged in (MERGE_HEAD). I have a strong suspicion that Git, too, stores merge parents in an ordered manner, but I have yet to verify this.

                    1. 2

                      Show unpushed commits:

                          unpushed = log --branches --not --remotes
                      

                      Handy for pipelines:

                          touched = show --name-only --pretty=format: --relative
                          conflicts = !git ls-files -u | cut -f2 | sort -u
                          staged = diff --cached --name-only --relative
                          untracked = ls-files --others --exclude-standard
                          modified = ls-files --modified --exclude-standard
                      

                      Overview of the repo’s branch structure:

                          overview = log --date=relative --simplify-by-decoration --all --graph