Explicitly super early, but if this got mature it would be really nice since Mercurial has a much nicer UX IMO. So I'd be very happy to use hg to control all of my git repositories. (I'd generally prefer to just use hg for everything, but unfortunately network effects are murder on tools that are explicitly meant for collaboration and integration; there isn't really a "mercurialhub", CI/CD support is lacking, etc.)
For those of us who don't know Mercurial -- how is the UX better than git's? Are there examples you can use to illustrate? (I've heard this frequently but never had a chance to really learn it.)
Mercurial uses a single command for a single purpose, generally speaking. Git overloads many different functions with clearly distinct uses in a single command.
A bit like having a word processor with a “format” command that changes text font, but is also used to produce a word count or print the document.
I think it attracts so much attention because git is forced on people by network effects, but many think the inconsistency of its interface defies belief, and that this lack of empathy for the user is symptomatic of a broader problem in designing software that seems to be prevalent across our industry.
Oh, enough bashing Git! It is for developers by developers. However you can teach cli newbies Hg management, but there's no chance to do the same with git.
The OXO brand of kitchen utensils was designed for people with arthritis. It became a giant worldwide brand because people realized that utensils that felt good for people with arthritis also felt good for everyone else.
Not everything is a tradeoff. Sometimes we can just do better.
Well, the biggest UX fail for me on git was the ambiguous and counter intuitive use of the command `checkout`. I've heard the latest versions of Git mitigate this with `git switch` and `git restore` but seriously `git checkout` is the most confusing command ever. Just attempt to describe to an SVNer what `git checkout` does in one sentence or less.
I haven't used Hg, but I'm guessing it doesn't have anything as bad as checkout.
Hm, I was hoping there would be higher-level differences than that. Like maybe the notion of having an index and staging area would be fundamentally different (or absent), or the notion of committing locally and pushing remotely would be fundamentally different, or something like that. Which is not to say command names aren't also confusing in git ('git add' to remove a file is my favorite), but they're not really where I imagined the substantial differences would lie?
> the notion of having an index and staging area would be fundamentally different (or absent)
Mercurial indeed lacks a staging area
> the notion of committing locally and pushing remotely would be fundamentally different
If you enable phases, it does. By default, all local commits are "draft" commits, which can have history edited. But when you push to (or pull from) a public repository, it becomes a "public" commit, and Mercurial will refuse to edit those commits. There's also a "secret" phase, which Mercurial will refuse to push to a public repository. You can also configure whether or not remote repositories are public or not.
The other fun feature is changeset evolution: when you rebase a changeset, Mercurial will keep the original changeset around with a link between the old and new versions of it. If you push the rebase remotely to a repository that previously had the original, Mercurial will tell people that changesets based on the original also need to be rebased when they pull it, and the history information will let them use the regular rebase flow to do it. Too bad BitBucket is dropping hg repos...
I'm sure someone will dispute that it is the same as the staging area, but mercurial does have the ability to only include some things in commits, and I use it daily. I admit I have no idea how it works on the command line, but in tortoisehg I can select individual files and hunks thereof to add to a commit (screenshot: https://imgur.com/ErPEjSN).
It's my impression that loads of people use this feature and somehow don't notice it exists, and will openly agree that mercurial lacks a staging area. My opinion is that defaulting to including changes from all files previously committed is such a sensible default that it makes the feature so seamless that people forget it exists even whilst using it.
Kinda like how people from the US think you can't turn left on red in Australia (equivalent of right on red in the US). You can, but the existence of slip lanes everywhere its allowed mean you don't actually face the red light when doing so. This hides it so effectively that I've seen several people complaining that they can't turn on red, not noticing that they do it every day.
Mecurial lets you choose which files you want to commit on the command line, either by simply specifying all the relevant globs, or by using -I (include) and -X (exclude) flags to compose a more complex pattern matching. You can also use interactive commit mode to open up a TUI to select which files you want to load. Alternatively, you can also add stuff to the top commit with `hg commit --amend`. git has basically all of these features.
What Mercurial doesn't have is a separate repository concept of an incomplete commit that has awkward, and often inconsistent, interactions with several common repository commands. Git has this, and this is the staging area.
The UI feature you use has nothing to do with the staging area, and is in fact closer to the `hg commit -i` interactive commit mode.
This makes for one difference which sometimes bites people even if they use the two tools in the same way: if you change a file "while committing it", e.g. changing it in one window while writing the commit message in another, Mercurial will commit the changed version, while Git of course commits the file as it was when you staged it, or when you ran commit -a.
I find this quite handy in Mercurial, as I sometimes realise while writing the commit message that e.g. I forgot to remove a line of temporary debug output, and I can still go and do it before I save the commit message. But that's very bad form that reflects badly on me. The way Git does it feels more correct.
Yes, hg's interactive commit allows this (and if you look at my screenshot, you can see tortoisehg presents checkboxes next to hunks in the diff, this is the way I use it)
Thinking back about my Git usage, I've indeed only ever used it as a sort of poor man's "hg commit --interactive", so I haven't missed it a bit when using Mercurial. Indeed I find Mercurial's model (there's only the repository with the real changesets and the working directory, but not this sort of weird inbetween thing called the staging area) much easier to reason about...
> Mercurial does have the ability to only include some things in commits
So do p4 and svn, through "changelists". The difference is that only in git is the staging area "persistent" from one command to another, and hidden from you when you run the default "what are the differences between my working directory and the head" command.
Mercurial does have a staging area, it is just turned off by default (and this is a good thing).
Mercurial has a very good extension system, and ships several useful extension in the default installation. The 'record' extension adds the 'commit individual hunks' functionality of the git staging area, and needs a one line "record=" in your config file to enable it. See: https://www.mercurial-scm.org/wiki/RecordExtension
By having some of these functionalities in extensions, the core interface is kept simpler and easier to learn, but the more powerful features are still there for those who want them.
I don't believe hg has an equivalent of the git index, although there are other ways to get similar functionality if you want it. The repository formats are different, of course, but it's easy enough to convert from one to the other. Git branches are mutable pointers that don't get recorded in the commit history, while in hg each commit records the branch it was committed to. hg has "bookmarks" that work like git's branches; I don't think git has any equivalent of hg-style branches.
For the most part, you can do anything with either one that you could do with the other, and the practical differences are mostly just in the UI, as well as a cultural component (e.g. hg more strongly discouraging history rewriting is part UI, part culture). Personally, I prefer the git UI because it has a more direct mapping onto manipulating a DAG of commits, but obviously not everyone feels that way, and I don't expect them to. When I first started with DVCS, I started with hg since it seemed easier to learn.
(Although really, my DVCS UI of choice is Magit, which I use for 99.9% of git stuff, to the point where I almost never need to actually run git myself on the command line.)
>> more direct mapping onto manipulating a DAG of commits
That would be a leaky abstraction. The fact that is using DAG should be irrelevant. If they find a better way of doing the work, that shouldn't really affect the ux.
I'm not sure I follow. A git history is literally a DAG of commits, and if you understand what each command is doing in terms of that DAG, there's very little abstraction at all, so there's nothing to leak.
The general consensus seems to be that git is an excellent semantic model wrapped in a ... mostly acceptable command-line UI. Even most big git fans aren't going to deny that the UI has some big warts (I'm a fan of git myself, there's _definitely_ things I'd change in a perfect world).
- [Major] I feel (but could be potentially convinced otherwise?) that there is one very deep fundamental flaw in the semantic model, and that is the fact that the identity of a commit depends on its history. I simply do not understand why this has to be the case. If I later discover a ZIP backup of the tree that I forgot to commit, and I want to insert it into the history, it shouldn't suddenly completely break the entire repo. Of course it seems fine to have a hash that depends on the history, and it's very likely useful for many purposes, but that shouldn't be the primary mechanism for identifying commits. By default, I think the identity of a commit should be defined by a hash of its contents only, but independent of its history. This would (among other things) let you re-write the history structure without rewriting the commits themselves and causing other people to have to reset their repos, which seems insanely useful to me.
I think the reason for why the commit hash has to change is that a commit represents the entire state of a repository, not just the change made in the commit. Being able to take a sequence of commits and insert them into a repository just is not a thing that makes sense in git's model.
If you just hashed diffs, you would not get whole-repo integrity guarantees.
It is possible to go the other way with patch theory (see Darcs) but it's far from trivial to implement performantly.
> I think the reason for why the commit hash has to change is that a commit represents the entire state of a repository, not just the change made in the commit.
Yes, of course I realize that's the reason. My entire point was that a commit shouldn't represent the entire state of a repository.
> Being able to take a sequence of commits and insert them into a repository just is not a thing that makes sense in git's model.
Yes, and this is exactly why I declared this to be a fundamental flaw in git's model.
> If you just hashed diffs
Diffs are an implementation concern, which I don't care about. I'm only talking about the logical semantics.
> you would not get whole-repo integrity guarantees.
As I explained, I wasn't suggesting you must get rid of that hash entirely: "Of course it seems fine to have a hash that depends on the history, and it's very likely useful for many purposes, but that shouldn't be the primary mechanism for identifying commits."
> It is possible to go the other way with patch theory (see Darcs) but it's far from trivial to implement performantly.
Again, I didn't say you have to get rid of the current hashes. I was just saying we need something else to use for identifying commits.
------
If an example helps: consider what happens when you (say) sign off on a commit. Are you genuinely signing off on the history? Can you even claim with a straight face that you even know everything in the history behind every commit you sign off on? The reality is, you don't, and you don't need to, because you're only concerned about the commit itself. There's no reason a change in history should invalidate your signature. (Of course, the point here is not just signatures. They're just one example to illustrate what I'm saying. You can think of other scenarios.)
No, you are signing off current state of the repository. Otherwise it would be possible (not trivial, but possible) to take signed commit and apply it on different history, which could create a security loophole.
Your view on commit is a logical set of changes. Git's view is state of the repository. The set of changes between revisions, which is useful for developer to see more than the whole state, is computed on the fly.
>I was just saying we need something else to use for identifying commits.
> Your view on commit is a logical set of changes. Git's view is state of the repository.
No, my view of a commit is not a logical set of changes. It's everything that would be in my worktree if I checked out the commit. Which is neither merely the changes from the previous commit(s), nor the entire history leading to the current commit.
But git already has this object, it’s called a tree and each commit has a unique tree associated with it. The commits are the object that carries history and metadata on top of the trees. Is your objection that the commit metadata is associated to the commit and not the tree?
I used to think that, but life gets complicated. How do you transmit a commit with its history? It used to be you just sent a single hash, now you would have to send all the commits of the whole history. Also how would you merge repositories with different histories?
I spend some time thinking about and I couldn't come up with anything sensible which wouldn't lead to history being effectively brokenm
I'm not sure I understand what the problem is. I'm only talking about the logical objects, not the physical representation. You can still store diffs and you can still have history hashes if that helps you with storage, processing, etc. -- that's perfectly fine. The storage optimizations should be independent of the logical structure. I'm just saying the logical identity of a commit shouldn't depend on its history. For example, if someone removes a commit from the history, that shouldn't have to trash anyone's repo and be such a massively destructive operation. It should only cause a client (in the worst case) to resync its history hashes from that point onward the next time it pulls -- which is quite a cheap, fast, and non-intrusive operation. (Say, 100k commits with 20B SHA-1 hashes would just be ~2MB.)
I'm not sure I understand. How could removing a commit from the history not be a destructive operation? It would necessarily affect every commit after it, hashes or no hashes, because for each commit following, the state of the tree would change, and thus so would the commit.
To my mind It would be akin to walking across the room and then somehow changing things such that you took one step fewer than required.
I'm not sure what kind of implementation you're envisioning that could work the way you seem to describe. Or do you mean that git should save the entire state of the repository as independent blobs every time you commit something? I don't think you could do that with any hope of reasonable performance.
If you instead just allowed "removing" commits logically without actually physically altering the datastructure on disk, there's no point in providing the functionality in the first place.
> If you instead just allowed "removing" commits logically without actually physically altering the datastructure on disk
Yes
> there's no point in providing the functionality in the first place.
Why so dismissive? Wouldn't it make sense to give me the benefit of the doubt here and ask me what the point of something like this might be, instead of just shutting down it down as pointless? Unless you think I'm just dumb, or otherwise trying to troll here by asking for something pointless?
I am not being dismissive. If you provide functionality that allows the user to delete something without actually deleting it, what's the point of pretending that you can delete things? Usually when people want to delete commits, it's because they committed something like a secret, and really do want to delete it.
Git doesn't try to hide the fact that the committed data is immutable, and to accomplish "deletion" the only option is to rewrite the entire affected part of the datastructure and garbage-collect anything that's unreferenced. You can not modify a commit. You can only create new commits and manipulate references to them.
This is fundamentally what enables git to function in a distributed manner, since the only state between repositories that needs special logic are the references; the actual data could be blindly synced with rsync or something, because it practically speaking can't ever conflict.
In order to have useful global non-hash commit identifiers, you would need a separate data structure of references that somehow decides which commits are identical, and is capable of reconciling conflicts globally across all clones of a git repository. I'm pretty sure that this isn't even in theory possible for the general case.
As for signoffs, a change in history might make a change you signed off broken or completely irrelevant, so yes, I do think that a change in history can invalidate a signoff on a commit.
What you ask for already exists: it's called the tree hash (which can be obtained by doing `git log -n1 --pretty="%T"`). The tree hash is unaffected by history, so if you for instance revert a commit, the tree hash will also revert. IIRC Julia uses tree hashes rather than commit hashes to track its packages.
I'm most definitely not suggesting we should be using patches instead of commits though. I don't want anything to be logically composed of patches at all. (Physical-storage-wise, they can go wild; I don't care.)
Ah, I misunderstood you. I thought you were asking for the identity of a commit to be the identity of the contents of that commit, i.e. the changes - but it seems you're talking about the contents of the working tree at the moment of the commit, with no dependency on prior history.
The thing is, the contents of a commit aren't patches. They are snapshots of the worktree. Your mental model is wrong (sorry), that's why you misunderstood. :-)
This is a common misconception that is corrected in many blog posts and tutorials; it's also explained clearly in the documentation. See the section (aptly) titled "Snapshots, Not Differences", where it says [1]:
"The major difference between Git and any other VCS (Subversion and friends included) is the way Git thinks about its data. Conceptually, most other systems store information as a list of file-based changes. These systems (CVS, Subversion, Perforce, Bazaar, and so on) think of the information they keep as a set of files and the changes made to each file over time. [...] Git doesn’t think of or store its data this way. Instead, Git thinks of its data more like a set of snapshots of a mini filesystem. Every time you commit, or save the state of your project in Git, it basically takes a picture of what all your files look like at that moment and stores a reference to that snapshot."
Now of course as an implementation detail it only stores diffs based on existing blobs, but except for the obvious speed difference, this fact is completely irrelevant to you as a user. You neither know nor care how it is actually storing its commits. And the thing is, even if you looked underneath, you would have absolutely no guarantee that the blobs are physically stored as diffs against the parent commits. They might be stored as diffs against other random blobs the repo for efficiency, and the user would be none the wiser.
What's the difference, though? How are patches different from commuting commits? By commuting I mean commits that do not depend on their position in history.
> - [Major] I feel (but could be potentially convinced otherwise?) that there is one very deep fundamental flaw in the semantic model, and that is the fact that the identity of a commit depends on its history.
Commits whose identity does not depend on their position in history are commits that are commutative (with respect to their position in history). So you very much said so, but we obviously appear to be talking about different things. I'm at a loss as to where these things differ.
What? This is like saying you and your younger brother are commutative. It makes no sense. Commits are snapshots, not diffs. i.e. they're variables, not operations. i.e. they're verbs, not nouns. They're as commutative as you and I are.
Oh, I see what you're saying now, I think. You're arguing for commits to completely lose any relationship with one another by default while remaining simple snapshots. I didn't realize this at first since I fail to see the immediate utility of this.
I agree the concept of a standalone snapshot is useful, but I don't think snapshots are the right abstraction when thinking about the evolution of a codebase from a human perspective and consider changes the more important concept.
I mean, the idea that commits are diffs is a (common) misconception about git, likely carried over from another VCS. The snapshot model is the current abstraction; I haven't added any idea of my own here here. It's right there in the documentation: https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3...
But I never said changes aren't important and should be neglected. And I'm also not saying there shouldn't be any relationship about commits' relationships to each other. You certainly can and should record and utilize that information as well. It just shouldn't be part of that commit. (Except maybe if it's a merge commit, in which case the contents of the previous file system snapshot are relevant. But even there, that shouldn't include the hash, which represents all the ancestors.) Information about commits' relationships should be external information, whose manipulation won't suddenly alter the commits or their identities themselves.
This isn't a radical proposal or something. For starters, git's own documentation (which I already linked here) literally say "Git thinks of its data more like a series of snapshots of a miniature filesystem". Well, the snapshot of the file system doesn't include the history of how it came into creation, so doesn't that mean that shouldn't be part of your commit? That's already a contradiction with its own principles right there. And going beyond that, most things we do with commits already revolve around the file system snapshots, not the history. e.g. when you sign a commit, you sign the snapshot, not the history. Or when you say this guy is the "committer", you're just talking about the snapshot, not the history. And when a commit gets inserted into the middle of the history, that logically doesn't affect you, and in practice, you don't want it to trash the commit you're one. The identity of your commit is still the same after all -- it's the same snapshot.
It seems like "one sentence or less" is an unreasonable demand in this case. But, if you're up for it, can you describe for an SVNer what the benefit of hg over svn is, you know, in one sentence or less? :-)
Both the first and the second are variants of the same action: make part of a workspace (defaulting to all of it) represent the specified point in the repository (defaulting to the current branch).
The first one is selecting a subset of the repository, exactly one file, and making the filesystem copy contain the contents that are in HEAD.
The second one makes the whole repository look like a different branch.
The third is just the second, along with a branch operation (which is weird to combine in it).
I’ll agree with you, though, just for a different command: git reset. That is a command that truly can’t be described in one sentence.
I think your comment illustrates well the problem. It was straightforward for me to understand what jfim said. It took a fair amount of effort to process your comment.
And the generalized (n variables) Taylor's theorem takes more effort to process than its single variable version.
If there was a command line tool for Taylor's theorem,I would prefer the one with generalized interface instead of the one that arbitrarily decided that n=1 was the standard case.
> When great thinkers think about problems, they start to see patterns. They look at the problem of people sending each other word-processor files, and then they look at the problem of people sending each other spreadsheets, and they realize that there’s a general pattern: sending files. That’s one level of abstraction already. Then they go up one more level: people send files, but web browsers also “send” requests for web pages. And when you think about it, calling a method on an object is like sending a message to an object! It’s the same thing again! Those are all sending operations, so our clever thinker invents a new, higher, broader abstraction called messaging, but now it’s getting really vague and nobody really knows what they’re talking about any more. Blah.
Or to put it another way, in this case git commands reflect what git needs to do, but hg commands reflect what the user wants to achieve. Which aligns with the "better UX" comments.
This has to be the top UX failure mode. Presenting the user with the abstractions we cleverly made in the code to factorize the architecture, instead of overlaying them with a different set of user-friendly abstractions.
It does make sense from that perspective (which seems to be modeled on cvs update), thanks for sharing it.
Both mercurial and subversion have update do the same thing (update the working copy to be the same as the one stored in the source control), and a revert verb (revert a file in the working copy to be the same as the one in the source control). Cvs and git instead use a single verb (update/checkout) for updating the entire tree or a set of files to a given revision. Interesting!
That's true, but from my perspective it makes more sense to attach combined operations to the first operation, not the following one.
For example, the equivalent of git fetch (fetch remote code without updating the working copy) is hg pull, while the equivalent of git pull (fetch remote code and update the working copy) is hg pull -u (pull, then update).
In my opinion, and this can be debated, is that it makes more sense to attach it to the first operation, since one would look at the documentation of the first command (how do I fetch the remote code?) then see that the working copy can be updated as part of that. The git documentation does mention git checkout -b as part of the git branch documentation, though.
Maybe this is just a rant against git though, at the end of the day, as long as work gets done, use whatever tools work for you. :-)
git checkout puts files from the repository into your working directory. For convenience, it will also update the HEAD ref if you check out the full contents of a branch.
I'm not sure what's so confusing about it. Are people thinking of their entire git workdir as the repository? I suppose the fact that .git is often in the same directory as the checkout might lead to people not thinking of them as separate entities.
> For convenience, it will also update the HEAD ref if you check out the full contents of a branch.
And updates the index. And sometimes it gets files from the index instead of a commit.
From a workflow perspective, "switch to working on a different branch", "copy some files into the working tree from a different commit", and "resolve all merge conflicts in this file in favor of 'ours' or 'theirs'" are three very different operations. From a low-level perspective they're somewhat similar, but not the same.
That is not a good description of what checkout does. :)
That is one of the things checkout can do, if you pass it a commit hash as the only argument. If you pass it a branch name, it does something quite different (a branch does not represent a specific point). If you pass it the path of a file, it does something quite different again. And depending on what extra flags you pass it, it'll do different things again, including creating a new branch, which is entirely different again.
> That is one of the things checkout can do, if you pass it a commit hash as the only argument.
No, that's all that git checkout does: check out prior commits.
Passing a branch name actually points to the branch HEAD, which marks a specific commit made in a specific point in time.
Passing a tag also does the same thing: check out a specific commit.
Passing a file path does the exact same thing: check out the contents of a specific file as saved in a specific commit represented by the branch HEAD. It's the exact same thing as git checkout <branch> or git checkout <tag>, except it does not update the whole working copy.
> And depending on what extra flags you pass it, it'll do different things again, including creating a new branch, which is entirely different again.
That's false as well. Running git checkout -b may be syntactic sugar for git branch && git checkout but what you are asking git to do, again, is to checkout a specific commit that represents the state of the repository at a specific point in time. That's it.
All git does is enable users to navigate and edit a graph of commits. It takes a poor mental model of git and a poor understanding of what git does to fail to understand that all git checkout does is checkout a commit done somewhere in the past.
Maybe not, but "checkout" still conjures up imagery of libraries, where you get permission to use materials. You "checkout" a book in order to read it. So in that sense, it sort of makes sense to "checkout" a branch.
But imagine bringing a book to a librarian and saying "hello, I'd like to checkout this book" and the librarian says "ok" and snatches the book from your hands and throws it into a furnace and tells you not to worry because she will order a new copy to be put on the shelf.
> because she will order a new copy to be put on the shelf.
Calling git checkout is a request to check out a specific version of the repository (or individual files) from a specific point in the repository's history.
If you ask the librarian "hello, I'd like to checkout this May 23rd issue of this magazine" then you can't act surprised that the librarian returns from the library's repository with the issue of the magazine released in May 23.
In 2010, I joined a project that was still using svn; so I used git-svn and hardly even noticed svn; and my colleagues would often ask me about project history because many things that were easy to check in gut weren’t easy in svn. Also, I had offline history and everything and they didn’t but thanks to git’s compressed storage, it didn’t take more disk space.
Sure there will. I doubt many people would use SVN for a new project, but plenty of old projects still use them. It's not always simple to migrate SVN to Git especially if you want to maintain the history.
FreeBSD uses SVN, and it is unlikely that they would ever move to git or mercurial, since they try to minimize the amount of copyleft code in the base system (e.g., they moved to Clang instead of GCC a few years ago)
One random example, to update the current working directory with changes and then (later) to revert a file is both done using `git checkout`, while it's `hg update` and `hg revert` respectively when using Mercurial.
The biggest UX improvement is that Mercurial's help is actually usable and useful to figure out how to do stuff. For git, your best bet is to search for your question and hope it's worded sufficiently well for someone to have already answered it correctly. As a concrete example of where this helps:
I needed to do a complex merge in a script, rather than manually, which means I need to know how to do a) automatic merge strategies (conflicts will happen were it naive!) and b) how to choose different strategies on a per-file basis. By running `hg help merge`, it tells me that `hg help merge-tools` will help me for problem a) and `hg help resolve` will help me for problem b). Read those documents, and you find the list of internal automatic merge strategies, as well as the fact that `hg resolve` allows you to change the current resolution status of a single file, or re-run a merge tool on that file. It's not clear to me how to do this in git just from following its help pages, despite `git merge` being far more voluminous than Mercurial's equivalents.
Another example of a power feature is that anywhere you can specify a revision (or set thereof), Mercurial has a full expression syntax for computing revisions. The cases most people are used to are the hash (or any unique prefix thereof), the commit's integer id [1], tip (for the most recent), . (for the currently-checked-out revision), or the tag, branch, or bookmark (Mercurial's equivalent to a git branch) name. But you can specify quite complex revision sets as well: `hg glog -r "ancestors(draft()) and (draft() or branchpoint())"` will show you a log with ASCII graph (the `glog` command) of all the changesets that are not yet public, as well as specifically where they all branched off. The syntax is naturally inscrutable, but once again, `hg help revisions` (as pointed out by the documentation in `hg log`) will give you the full details.
Another surprisingly useful UX feature is that Mercurial is not afraid to alias the hell out of everything. `hg rename`, `hg move`, and `hg mv` all do exactly the same thing: record a file move. `hg glog` is short for `hg log --graph`. And any unambiguous prefix of a command is happily expanded to that command--useful if you don't remember if the document name is 'hg help revision' or 'hg help revisions', as they both work!
For repository features itself, there's a few things that really make things simpler for people:
* Mercurial lacks the staging area. This is a good thing.
* The UI generally aligns to the naming conventions of SVN, which makes it much easier for people transitioning from SVN.
* Mercurial treats commits as a much more permanent thing. There's no such thing as garbage collection in Mercurial, and you don't run the risk of losing commits just because you're not currently on a "branch" [2] like in git. Also, no scary message like "DETACHED HEAD STATE" if you decide to go poking at an older commit in Mercurial.
* Phases and changeset evolution make history rewriting much safer: you can't rewrite public history (determined by the phase), and changeset evolution means that pushing a rebase allows other people to safely develop on top of draft changes. It's made quite evident when you pull that your changesets are based on ones that have changed history, so you need to continue the rebase to stabilize your branch.
[1] All commits are assigned a unique, incrementing integer ID from 1 to N. These IDs are not stable: if you delete an integer, everything afterwards is shifted down. And if you clone a repository, it won't necessarily assign the same IDs. But they're still useful for referring to commits locally.
[2] Mercurial's branches are a completely different beast from git's branches: they are immutable properties of a changeset, and they are designed to be used for things such as release branches or version branches rather than feature branches. The Mercurial equivalent to a git branch is a bookmark. Hence why "branch" is quoted here--I'm referring to it in the git sense, not the Mercurial sense.
One of the more noticeable differences is that there is no staging area. So to commit your changes you only need yo go through a single step,hg commit.
For the situations where you actually only want to partially commit things you can use "hg shelve", which is sort of like "git stash"
I'm not sure if tortoisehg (the GUI client everyone I know who uses hg uses) is using shelve internally for this, but it presents an interface where you can select individual files and hunks for a commit. Link to my previous comment on it:
https://news.ycombinator.com/item?id=21393972
Agreed. Git's stage provides more flexibility when committing - I'm not constrained to either commit, stash, or revert every modified file. I can choose which modified files I'd like to include in the commit, and the others can be left as-is.
It's very nice to have when I've done a lot of work that most naturally should be split into more than one commit, or when I have temporary modifications (say, to config files) that I'd like to leave in place for now but not include as part of the commit.
I'm confused. So say we have a repo that contains only 3 files, foo, bar, and baz. We modify all 3 files but for 2 unrelated reasons. In git, you could do this as `git add foo ; git add bar ; git commit`. But you could also just `git commit foo bar ; git commit baz`. And in mercurial, `hg commit foo bar ; hg commit baz` works fine. Am I misunderstanding what you want to do?
Sometimes I might stage only part of the changes in a single file. Sometimes I want to stage a file or two, see what’s remaining, and decide what else should be in this particular commit. I can and do diff only staged changes, then diff only unstaged changes, to decide how to proceed. Sometimes I unstage certain changes if I decide that no, they in fact should not be in this next commit.
Basically, it’s a scratchpad that’s gives me space to think and plan.
Agreed. After getting used to that model for building up a particular commit, I wouldn't want to go back to the mercurial way.
I used to use mercurial back in the day, before git took over the planet. Switching back to it would be slightly analogous to switching keyboard layouts at this point. Sure, qwerty is arbitrary and sort of stupid, but I've spent so much time using it that I've gotten used to where everything is.
You have a git workflow that works for you, which is great. But you can't really compare unless you use the appropriate hg features to accomplish the same things. You're talking about a bunch of use cases, so I'd have to throw a bunch of commands at you. But basically, I do all of that, every day, with hg commands and I really dislike the added mental overhead of the staging area every time I use git. (Not that I'd try to use git without it; common usage is built around it.)
In short, for similar scenarios I'd do `hg commit -i` to select a couple pieces I was sure I wanted in the commit, then `hg diff` to see remaining pieces or `hg diff -c .` to see my commit so far, then `hg amend -i` to select additional fragments or files. To undo some of that, I'd use `hg uncommit`. Same effect, fewer concepts -- your stage/index/cache is just materialized as a plain everyday commit that you can decide is done at any time.
I'm not going to argue that suddenly every vcs task becomes trivial with hg. Far from it. And these discussions often get bogged down when people argue from the standpoint "I only need X so everything else is unneeded complexity" or "you need all of this stuff because you might need to collaborate with multiple remote people editing the same files who need to back out some of your changes while they're working and have everything magically put back together at the end, oh and we need full control over what the final history looks like too." These VCS systems have to deal with a wide range of complexity, and reductive takes like "you really only need these 3 commands; why is it so hard??" don't illuminate anything. (Note that I'm not addressing this at your comment, which was not at all doing that.)
I don't often need this, but in Hg I simply use commits for that and use histedit to clean up later. The staging area basically is just a commit, just one that's not part of your history yet.
Mercurial does have a staging area. It just defaults to including all previously-committed files, which IMHO is very sensible. Link to my previous comment on it:
What you describe is not a staging area, it is an interactive commit.
And, personally, I see no positive value in a staging area, given that you can achieve the same functionality via interactive commits or amending commits.
Ah, fair enough. So I suppose git's staging area has the additional benefit that you can stage some things and then shut your computer down and the staging will be there when you come back.
I have no idea whether tortoisehg remembers the state of what you've selected in its 'interactive commit' GUI, because I've only ever made those decisions when committing. Given I've been using it almost daily for about six years on a community project (i.e. not just linear commits by a single dev who doesn't need to care about anyone else), I think this is also evidence that a persistent staging area isn't super important.
staging interacts with other commands as well. You can stage a bunch of stuff then revert or stash the rest, you can stash everything in the staging area, you can diff only stuff in the staging area and so on.
It basically acts as a clipboard. Possibly hg interactive commits work in the same way, but then I do not know how that would be different from a staging area.
It isn't. The thing people supposedly like is Mercurial's heavy-duty branching model, but mostly I find that Mercurial so wants to hide the details of the underlying store that it just gets in the way. Same thing with Fossil -- great internals, opinionated UI, useless to me.
Mercurial resisted rebase forever, and then they insisted that rebasing and editing history must be separate operations (why? opinions). And Mercurial doesn't have anything like an index, so no git add -e/-p, but instead you can do a combined equivalent of git add -p and git commit with hg record -- talk about "one command does one thing" nonsense.
From what I've seen, few people use Mercurial's heavy branches, except for things like release and maintenance branches, where it makes a certain amount of sense. Some do, but most people seem to use bookmarks, topics, or multiple heads.
Everything else you're talking about sounds like ancient history to me. `hg record` is now `hg commit -i`, so it's back to one command. `hg histedit` intersects with rebase, in that you can do some types of rebasing with it, but you tend to use them in different scenarios.
> `hg histedit` intersects with rebase, in that you can do some types of rebasing with it, but you tend to use them in different scenarios.
I don't tend to use them in different scenarios.
I routinely `git rebase -i origin/master`. Sometimes from detached-HEAD mode.
> `hg record` is now `hg commit -i`, so it's back to one command.
So Mercurial has an index? Or is that something else? Looks like roughly the same thing as `hg record -p`. An index is much easier to use. `git add -e` is simply amazing.
I've tried to use "bookmarks" in hg or whatever they were calling light-weight branches, but I've had trouble with it.
Git won.
I'm not saying Git is perfect. It's missing a few things, but it's better than all open source VCSes so far. And the work MSFT is doing is really helping too.
> I routinely `git rebase -i origin/master`. Sometimes from detached-HEAD mode.
Which would both use `hg rebase`, so I'm not sure what you mean. Histedit is for reordering commits, or dropping some from the middle of a stack, and/or folding adjacent commits together. All in one go, if you so choose. All of which you could do with multiple rebase invocations, but histedit is an easier declarative interface to multiple rebase operations. On the flip side, it's limited to operating on linear chunks of the dag.
> So Mercurial has an index? Or is that something else?
Something else. i for interactive.
> Looks like roughly the same thing as `hg record -p`
Do you mean git commit -p? (Or is it git add -p? I can't check right now, and I can't even predict which one git would pick.) Anyway, yes, it's exactly that.
> An index is much easier to use.
Why? I don't miss it in hg. I use it in git, but it mostly feels like it's an extra thing to remember. Yet most git users agree with you, so I'd like to understand why.
> > An index is much easier to use.
>
> Why? I don't miss it in hg. I use it in git, but it mostly feels like it's an extra thing to remember. Yet most git users agree with you, so I'd like to understand why.
With `hg record`, if anything goes wrong, I have to start over. That can be extremely frustrating.
With `git add -e/-p` I can quit at any time and leave the things that were OK in the index while I take time to think about how to "absorb" the rest, or even commit what I have now and then go about dealing with the rest.
The index not only gives me the ability to do that sort of thing, but it also lets me get atomicity -- no matter what happens in the workspace, the index stays as I left it until I either add more to it or commit it or reset it.
It's not. hg users generally don't understand that git is doing something fundamentally different from hg. git adds a 3rd dimension to your file system. dimension 1 = files, dimension 2 = directories, dimension 3 = branch. That git adds this 3rd dimension makes it useful for version control but that's effectively a side effect. hg on the other hand is specifically about version control so it does not handle adding this 3rd dimension well. This is why for example things like gh-pages on github where there's a branch that has absolutely nothing to do with any other branch and no shared history is a normal thing to do in git (because a branch is another dimension to the file system, not a version control construct).
That difference, adding branches as a 3rd dimension, is why git is more powerful. It's also a reason why a staging area is important to be able to precisely decide what goes in each branch since they may be unrelated. That is not to say that hg's ux isn't easier to understand than git's. It is easier. In part because it's trying to do something else. That is also not to excuse git's unintuitiveness.
That git adds this 3rd dimension makes it useful for version control but that's effectively a side effect. hg on the other hand is specifically about version control so it does not handle adding this 3rd dimension well.
Not true in the least.
hg has the same dimensions that git has, just implemented differently. hg’s branching model is actually a superset of what git does.
A git branch is essentially the same as Mercurial's bookmarks. Mercurial also has named branches which are permanent and anonymous branching, which git doesn't have. You can read all about them at http://stevelosh.com/blog/2009/08/a-guide-to-branching-in-me....
Just to clarify, in mercurial you can "branch off" and then merge back just like in git. The difference is that git has an explicit concept of "branch" and each commit must be part of a branch, whereas in hg each commit stands on its own and your history is just a graph of commits. Instead of using branches to indicate that you are working on different features, in hg you use bookmarks, which are just pointers to commits. In hg land, gh-pages would be a bookmark.
I'm no expert, but isn't a branch in git just a label (i.e. a pointer) for a commit? In fact you can have orphaned commits in your reflog that are not pointed by any branch (and will be GC'd at some point).
I guess you mean that hg commits do not have parent pointers and are thus not intrusively chained in trees, while an hg branch is a separate non-intrusive construct?
> It's not. hg users generally don't understand that git is doing something fundamentally different from hg.
> That is not to say that hg's ux isn't easier to understand than git's. It is easier. In part because it's trying to do something else. That is also not to excuse git's unintuitiveness.
1. So... it is, albeit perhaps by sacrificing certain features.
2. I'm not convinced that git's admissibly more powerful branching model justifies the claim that hg's UX is no nicer and people who prefer it just don't understand git. If nothing else, what practical difference is there between creating a branch with no history vs creating a branch with history and then clearing it out? (That is: `hg branch newbranch && rm -r * && hg commit . -m 'new start'`) Both hg and git boil down to DAGs and have generally similar tools available to manage the grand set of commits.
I suppose Mercurial’s Phases [1] are another dimension—each commit can have the status of draft, public or secret. This is part of Mercurial’s Changeset evolution feature[2].
Your commits are in the Draft phase, which means they can be changed at any time. By default (this is configurable) when you push to your team’s repo, those changes become Public, which means they won't change and it’s safe to code or rebase on top of them.
hg-git already works well, but has some issues around updates (since it's maintenance is lagging a bit, so I hope this change will replace it as a better maintained option).
I've been using Mercurial nearly exclusively for interacting with git/github since, well, forever. The only case that breaks for me is git repos with submodules, which is the only reason I haven't been able to completely avoid git.
Wasn't bitbucket pretty much that at first? Then they added git support because in our day and age you need git support if you want to be relevant as a code repo hosting solution.
I've been assuming so; the official page overwhelmingly describes it, explicitly, as a git tool, it's not like it has broad support (ex. for CVS, SVN, etc.), and honestly in their shoes I'd probably want to remove all that code that no longer is useful for working with any of their products.
As sibling comment notes, that's going away. Even when it was around (and in my mind is now replaced by sr.ht), the problem is that 90% of projects (not a real number, obviously) were using git hosted on github, so if I want to contribute, then I'm going to use git and almost have to have a github account. There were never a huge number of projects hosted on BB, and an even smaller slice of those used hg.
It'd be interesting to see how they deal with unnamed heads.
git creates them in the background pretty frequently, and expects them to be invisible to user (for example during many rebases or when viewing remote repositories, or when pulling remote changes)
mercurial creates them in response to user action, displays them to user by default, and expects users to care about them.
This is only one of a number of differences in the underlying ontologies of mercurial and git. Another is branches: git repositories have no concept of a "branch" in the mercurial sense, what git calls a "branch" is more like an hg bookmark. I'm not sure how a tool like this can deal with such things without either disabling some hg functionality or adding metadata to keep track of the hg stuff that git doesn't track.
The branch is a wad of metadata included in (attached to?) the commit. It'll somehow have to roundtrip the various metadata.
I doubt it'll ever be 100% complete, but you should be able to get quite a ways. Certainly far enough to make it work using github as your repo of record (and as someone who regularly submits github PRs via hg-git, I have evidence that it can work.)
I don’t think hg has the concept of unnamed heads in any sort of acceptable manner. You can have 2 heads with the same branch name, which I suppose is called an “unnamed head” but your team will berate you if you force push an unnamed head. (You can’t push an unnamed head without force. Using force almost always means you shouldn’t do whatever you’re doing) when git encourages using ‘force’ I say a silent prayer
In a way, this is backwards. hg is and has always been totally fine with multiple unnamed heads; git is the one that insists that topological heads have some ref pointing at them.
But I agree with your basic point. Unnamed heads in git and unnamed heads in hg are not treated the same, and could be easy for tools to mix up. (I hadn't realized that this was even possible in git??)
I really hope this becomes a debugged real tool. Mercurial's UI makes intuitive sense and git's is just a bunch of magic incantations. Git's builtin help and documentation are also useless. I can easily use Mercurial offline without a Google search window open. That's impossible for me with git.
Having used both extensively, that sounds awfully hyperbolic. Neither is perfect but “magic incantations” is really a stretch — to be honest, it sounds like you’re reading with intention of looking for reasons to dislike it.
I started with Mercurial at least a year before Git and found roughly as many areas where I preferred either one. Having taught a number of people how to use the basics over the years, I’m highly skeptical of claims that either is a substantial barrier compared to learning how to use a version control system and work well with other contributors.
I switched to mercurial a couple years back, and generally found it a much smoother and nicer experience. It can't do anything that git can't do, but the basic operations felt much better.
A summary of the stuff I liked: histedit feels much more natural than rebase -i, and I rarely need to look up arguments.
When rebasing, I frequently found that I needed to look up what was what of the three refs git can take (source, dest and onto).
hg undo, unamend and split take complicated flows in git and make them trivial.
I don't think I ever got as good an understanding of the hg model as I had with git, but I had to get that understanding because I screwed up my repo and wanted to fix it. I've never done that in hg.
See, this is a useful comment: it avoids hyperbole and has specific details people can understand even if they don’t agree with every point.
I have only once had a VCS lose data, but that was amusingly the opposite: mercurial, before they had a standard rebase-like mechanism, so the only takeaway I’d draw is that a feature like that needs to be very well tested.
> Context switching between git and anything else at this point for me is a considerable waste of my time.
My last job used a mix of SVN, Clearquest (with and without UCM) and Git for different projects. It wasn't that big of a deal. You just need to learn the concepts if there are new ones and how to use the commands/GUI.
At the end of the day, it's just a version control system. People are far too invested in their tools.
You haven't checked out fig yet? It's an hg frontend for your monorepo and a little birdie tells me that within google it's a lot more popular than git5
The only problem with this is that pygit2 needs a lot of love. The performance of pygit2 is kind of weak, and it doesn't even support everything libgit2 supports.
Maybe this will be the kick in the pants to get pygit2 to be better maintained?
With BitBucket sunsetting hg repos (creation disabled in February 2020, all repos removed in June 2020), it would be quite timely if this extension matures in short order.
I haven't used hg enough to have an opinion on it, despite several attempts... Problem is I learn bottom up, and I just haven't been able to "think in mercurial" the way I can "think in git".
I find it interesting that what git calls a commit is actually a revision (or checkpoint, snapshot, point-in-time) and what mercurial calls a revision is actually a commit (or patch, delta, changeset).
I think a lot of people think in terms of patches/changesets and I suspect (still haven't gotten far enough to confirm) hg is a toolbox for managing them in a similar way to how git is a toolbox for manipulating its snapshot based DAG.
I find git conceptually simpler, but honestly git and mercurial are pretty close to each other. I've been giving some thought to Fossil recently and it really seems like the industry missed an opportunity to question some assumptions and biases about what our tools should provide. For instance, neither mercurial or git contain issue/bug trackers, but Fossil does.
> For instance, neither mercurial or git contain issue/bug trackers, but Fossil does.
I’m glad git doesn’t. It’s a separate concern and breaks the “do one thing, do it well” UNIX philiosophy.
People can’t agree on a schema for issues/bug-trackers as it is, and you’ll have the ISO 9001-compliant folks who feel compelled to have 150 different hierarchical fields and 20 different issue classes with the Github issue pragmatists happy with only using non-hierarchical non-namespace tags. You could accommodate them all by using a schemaless or custom schema support, but then you’ll have people fighting over how to implement custom schemas, and then having to get people to work on building that feature.
It would also be inappropriate for security-sensitive issues or other cases where compartmentalising information is necessary. (And encrypting the data doesn’t help once the key gets leaked)
If you really want bug-tracking in-repo, you can still do that with a Text-file tree or Sqlite DB in a separate branch (probably a good idea to have a separate checkout to avoid expensive `git checkouts` when the entire tree changes.
Doesn't have two separate concepts for "branches" and "bookmarks" which is extremely confusing compared to the simple Git DAG.
Not having to add crucial functionality like stashes by editing the config to add a new module.
Better UI. For example to get full hash of the current commit, the recommended way to do it in git is `git rev-parse HEAD`. In hg it's `hg log -l 1 --template '{node}\n' -r .` Yes really.
Git's idea of branches confuses a lot of new users. Their expect them to work like Mercurial's. Bookmarks were added for Git compatibility.
The Mercurial command to get the short hash of the current commit is "hg id -i". You can get the long hash by adding "—debug". The "hg log" command shows a more general way to do it and could be shorter.
You'd think it would be `hg id`, but it doesn't work without connection to remote repository and is extremely slow with large repositories with uncommited changes, so it's not the correct answer. --debug option also prints info other than hash and can't be customized.
>Mercurial logs are also easy to customize.
I wanted to customize logging string so that it shows (sequential commit id) - (other stuff), but there's no way to make sequential commit id to line up neatly when it goes from 9 to 10, or from 99 to 100. So making a pretty one-line log is impossible. Maybe it was fixed since then, I wouldn't know.
I thought Mercurial operated on higher-level concepts, such as having branches be first-class objects? In fact, my impression was that Mercurial made it particularly difficult to edit history and required plugins to do so.
Mercurial represents history as a DAG. You can implicitly create unnamed branches and explicitly create named branches. Committing to a named branch records the branch name as part of the changeset.
Extensions are just how Mercurial commands are implemented. The extensions for editing history are included and supported. The default configuration is just very conservative.
I've been looking for an easy way of keeping git mirrors of my hg repos, and this seems like it would be a huge improvement over the previous extension! Here's to hoping it lands.