{"thread":{"id":"50372","subject":"[PATCH v4 1/8] technical doc: add a design doc for the evolve command","startedAt":"2019-02-01T03:09:33Z","lastAt":"2019-02-01T15:46:10Z","messageCount":9,"participants":["sxenos@google.com","Ramsay Jones"],"isPatch":true,"patchVersion":4,"patchTotal":8},"messages":[{"id":"368287","messageId":"20190201030925.177124-1-sxenos@google.com","threadId":"50372","inReplyTo":null,"subject":"[PATCH v4 1/8] technical doc: add a design doc for the evolve command","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:18Z","receivedAt":"2019-02-01T03:09:33Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nThis document describes what a change graph for\ngit would look like, the behavior of the evolve command,\nand the changes planned for other commands.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n Documentation/technical/evolve.txt | 1034 ++++++++++++++++++++++++++++\n 1 file changed, 1034 insertions(+)\n create mode 100644 Documentation/technical/evolve.txt\n\ndiff --git a/Documentation/technical/evolve.txt b/Documentation/technical/evolve.txt\nnew file mode 100644\nindex 0000000000..7967c73e5d\n--- /dev/null\n+++ b/Documentation/technical/evolve.txt\n@@ -0,0 +1,1034 @@\n+Evolve\n+======\n+\n+Objective\n+=========\n+Create an \"evolve\" command to help users craft a high quality commit history.\n+Users can improve commits one at a time and in any order, then run git evolve to\n+rewrite their recent history to ensure everything is up-to-date. We track\n+amendments to a commit over time in a change graph. Users can share their\n+progress with others by exchanging their change graphs using the standard push,\n+fetch, and format-patch commands.\n+\n+Status\n+======\n+This proposal has not been implemented yet.\n+\n+Background\n+==========\n+Imagine you have three sequential changes up for review and you receive feedback\n+that requires editing all three changes. We'll define the word \"change\"\n+formally later, but for the moment let's say that a change is a work-in-progress\n+whose final version will be submitted as a commit in the future.\n+\n+While you're editing one change, more feedback arrives on one of the others.\n+What do you do?\n+\n+The evolve command is a convenient way to work with chains of commits that are\n+under review. Whenever you rebase or amend a commit, the repository remembers\n+that the old commit is obsolete and has been replaced by the new one. Then, at\n+some point in the future, you can run \"git evolve\" and the correct sequence of\n+rebases will occur in the correct order such that no commit has an obsolete\n+parent.\n+\n+Part of making the \"evolve\" command work involves tracking the edits to a commit\n+over time, which is why we need an change graph. However, the change\n+graph will also bring other benefits:\n+\n+- Users can view the history of a change directly (the sequence of amends and\n+  rebases it has undergone, orthogonal to the history of the branch it is on).\n+- It will be possible to quickly locate and list all the changes the user\n+  currently has in progress.\n+- It can be used as part of other high-level commands that combine or split\n+  changes.\n+- It can be used to decorate commits (in git log, gitk, etc) that are either\n+  obsolete or are the tip of a work in progress.\n+- By pushing and pulling the change graph, users can collaborate more\n+  easily on changes-in-progress. This is better than pushing and pulling the\n+  changes themselves since the change graph can be used to locate a more\n+  specific merge base, allowing for better merges between different versions of\n+  the same change. \n+- It could be used to correctly rebase local changes and other local branches\n+  after running git-filter-branch.\n+- It can replace the change-id footer used by gerrit.\n+\n+Goals\n+-----\n+Legend: Goals marked with P0 are required. Goals marked with Pn should be\n+attempted unless they interfere with goals marked with Pn-1.\n+\n+P0. All commands that modify commits (such as the normal commit --amend or\n+    rebase command) should mark the old commit as being obsolete and replaced by\n+    the new one. No additional commands should be required to keep the\n+    change graph up-to-date.\n+P0. Any commit that may be involved in a future evolve command should not be\n+    garbage collected. Specifically:\n+    - Commits that obsolete another should not be garbage collected until\n+      user-specified conditions have occurred and the change has expired from\n+      the reflog. User specified conditions for removing changes include:\n+      - The user explicitly deleted the change.\n+      - The change was merged into a specific branch.\n+    - Commits that have been obsoleted by another should not be garbage\n+      collected if any of their replacements are still being retained.\n+P0. A commit can be obsoleted by more than one replacement (called divergence).\n+P0. Must be able to resolve divergence (convergence).\n+P1. Users should be able to share chains of obsolete changes in order to\n+    collaborate on WIP changes.\n+P2. Such sharing should be at the user’s option. That is, it should be possible\n+    to directly share a change without also sharing the file states or commit\n+    comments from the obsolete changes that led up to it, and the choice not to\n+    share those commits should not require changing any commit hashes.\n+P2. It should be possible to discard part or all of the change graph\n+    without discarding the commits themselves that are already present in\n+    branches and the reflog.\n+P2. Provide sufficient information to replace gerrit's Change-Id footers.\n+\n+Similar technologies\n+--------------------\n+There are some other technologies that address the same end-user problem.\n+\n+Rebase -i can be used to solve the same problem, but users can't easily switch\n+tasks midway through an interactive rebase or have more than one interactive\n+rebase going on at the same time. It can't handle the case where you have\n+multiple changes sharing the same parent when that parent needs to be rebased\n+and won't let you collaborate with others on resolving a complicated interactive\n+rebase. You can think of rebase -i as a top-down approach and the evolve command\n+as the bottom-up approach to the same problem.\n+\n+Several patch queue managers have been built on top of git (such as topgit,\n+stgit, and quilt). They address the same user need. However they also rely on\n+state managed outside git that needs to be kept in sync. Such state can be\n+easily damaged when running a git native command that is unaware of the patch\n+queue. They also typically require an explicit initialization step to be done by\n+the user which creates workflow problems.\n+\n+Mercurial implements a very similar feature in its EvolveExtension. The behavior\n+of the evolve command itself is very similar, but the storage format for the\n+change graph differs. In the case of mercurial, each change set can have one or\n+more obsolescence markers that point to other changesets that they replace. This\n+is similar to the \"Commit Headers\" approach considered in the other options\n+appendix. The approach proposed here stores obsolescence information in a\n+separate metacommit graph, which makes exchanging of obsolescence information\n+optional.\n+\n+Mercurial's default behavior makes it easy to find and switch between\n+non-obsolete changesets that aren't currently on any branch. We introduce the\n+notion of a new ref namespace that enables a similar workflow via a different\n+mechanism. Mercurial has the notion of changeset phases which isn't present\n+in git and creates new ways for a changeset to diverge. Git doesn't need\n+to deal with these issues, but it has to deal with picking an upstream branch as\n+a target for rebases and protecting obsolescence information from GC. We also\n+introduce some additional transformations (see obsolescence-over-cherry-pick,\n+below) that aren't present in the mercurial implementation.\n+\n+Semi-related work\n+-----------------\n+There are other technologies that address different problems but have some\n+similarities with this proposal.\n+\n+Replacements (refs/replace) are superficially similar to obsolescences in that\n+they describe that one commit should be replaced by another. However, they\n+differ in both how they are created and how they are intended to be used.\n+Obsolescences are created automatically by the commands a user runs, and they\n+describe the user’s intent to perform a future rebase. Obsolete commits still\n+appear in branches, logs, etc like normal commits (possibly with an extra\n+decoration that marks them as obsolete). Replacements are typically created\n+explicitly by the user, they are meant to be kept around for a long time, and\n+they describe a replacement to be applied at read-time rather than as the input\n+to a future operation. When a replaced commit is queried, it is typically hidden\n+and swapped out with its replacement as though the replacement has already\n+occurred.\n+\n+Git-imerge is a project to help make complicated merges easier, particularly\n+when merging or rebasing long chains of patches. It is not an alternative to\n+the change graph, but its algorithm of applying smaller incremental merges\n+could be used as part of the evolve algorithm in the future.\n+\n+Overview\n+========\n+We introduce the notion of “meta-commits” which describe how one commit was\n+created from other commits. A branch of meta-commits is known as a change.\n+Changes are created and updated automatically whenever a user runs a command\n+that creates a commit. They are used for locating obsolete commits, providing a\n+list of a user’s unsubmitted work in progress, and providing a stable name for\n+each unsubmitted change.\n+\n+Users can exchange edit histories by pushing and fetching changes.\n+\n+New commands will be introduced for manipulating changes and resolving\n+divergence between them. Existing commands that create commits will be updated\n+to modify the meta-commit graph and create changes where necessary.\n+\n+Example usage\n+-------------\n+# First create three dependent changes\n+$ echo foo>bar.txt && git add .\n+$ git commit -m \"This is a test\"\n+created change metas/this_is_a_test\n+$ echo foo2>bar2.txt && git add .\n+$ git commit -m \"This is also a test\"\n+created change metas/this_is_also_a_test\n+$ echo foo3>bar3.txt && git add .\n+$ git commit -m \"More testing\"\n+created change metas/more_testing\n+\n+# List all our changes in progress\n+$ git change list\n+metas/this_is_a_test\n+metas/this_is_also_a_test\n+* metas/more_testing\n+metas/some_change_already_merged_upstream\n+\n+# Now modify the earliest change, using its stable name\n+$ git reset --hard metas/this_is_a_test\n+$ echo morefoo>>bar.txt && git add . && git commit --amend --no-edit\n+\n+# Use git-evolve to fix up any dependent changes\n+$ git evolve\n+rebasing metas/this_is_also_a_test onto metas/this_is_a_test\n+rebasing metas/more_testing onto metas/this_is_also_a_test\n+Done\n+\n+# Use git-obslog to view the history of the this_is_a_test change\n+$ git log --obslog\n+93f110 metas/this_is_a_test@{0} commit (amend): This is a test\n+930219 metas/this_is_a_test@{1} commit: This is a test\n+\n+# Now create an unrelated change\n+$ git reset --hard origin/master\n+$ echo newchange>unrelated.txt && git add .\n+$ git commit -m \"Unrelated change\"\n+created change metas/unrelated_change\n+\n+# Fetch the latest code from origin/master and use git-evolve\n+# to rebase all dependent changes.\n+$ git fetch origin master\n+$ git evolve origin/master\n+deleting metas/some_change_already_merged_upstream\n+rebasing metas/this_is_a_test onto origin/master\n+rebasing metas/this_is_also_a_test onto metas/this_is_a_test\n+rebasing metas/more_testing onto metas/this_is_also_a_test\n+rebasing metas/unrelated_change onto origin/master\n+Conflict detected! Resolve it and then use git evolve --continue to resume.\n+\n+# Sort out the conflict\n+$ git mergetool\n+$ git evolve --continue\n+Done\n+\n+# Share the full history of edits for the this_is_a_test change\n+# with a review server\n+$ git push origin metas/this_is_a_test:refs/for/master\n+# Share the lastest commit for “Unrelated change”, without history\n+$ git push origin HEAD:refs/for/master\n+\n+Detailed design\n+===============\n+Obsolescence information is stored as a graph of meta-commits. A meta-commit is\n+a specially-formatted merge commit that describes how one commit was created\n+from others.\n+\n+Meta-commits look like this:\n+\n+$ git cat-file -p <example_meta_commit>\n+tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n+parent aa7ce55545bf2c14bef48db91af1a74e2347539a\n+parent d64309ee51d0af12723b6cb027fc9f195b15a5e9\n+parent 7e1bbcd3a0fa854a7a9eac9bf1eea6465de98136\n+author Stefan Xenos <sxenos@gmail.com> 1540841596 -0700\n+committer Stefan Xenos <sxenos@gmail.com> 1540841596 -0700\n+parent-type c r o\n+\n+This says “commit aa7ce555 makes commit d64309ee obsolete. It was created by\n+cherry-picking commit 7e1bbcd3”.\n+\n+The tree for meta-commits is always the empty tree whose hash matches\n+4b825dc642cb6eb9a060e54bf8d69288fbee4904 exactly, but future versions of git may\n+attach other trees here. For forward-compatibility fsck should ignore such trees\n+if found on future repository versions. Similarly, current versions of git\n+should always fill in an empty commit comment and tools like fsck should ignore\n+the content of the commit comment if present in a future repository version.\n+This will allow future versions of git to add metadata to the meta-commit\n+comments or tree without breaking forwards compatibility.\n+\n+Parent-type\n+-----------\n+The “parent-type” field in the commit header identifies a commit as a\n+meta-commit and indicates the meaning for each of its parents. It is never\n+present for normal commits. It contains a space-deliminated list of enum values\n+whose order matches the order of the parents. Possible parent types are:\n+\n+- c: (content) the content parent identifies the commit that this meta-commit is\n+  describing.\n+- r: (replaced) indicates that this parent is made obsolete by the content\n+  parent.\n+- o: (origin) indicates that this parent was generated from the given commit.\n+- a: (abandoned) used in place of a content parent for abandoned changes. Points\n+  to the final content commit for the change at the time it was abandoned.\n+\n+There must be exactly one content or abandoned parent for each meta-commit and it is\n+always the first parent. The content commit will always be a normal commit and not a\n+meta-commit. However, future versions of git may create meta-commits for other\n+meta-commits and the fsck tool must be aware of this for forwards compatibility.\n+\n+A meta-commit can have zero or more replaced parents. An amend operation creates\n+a single replaced parent. A merge used to resolve divergence (see divergence,\n+below) will create multiple replaced parents. A meta-commit may have no\n+replaced parents if it describes a cherry-pick or squash merge that copies one\n+or more commits but does not replace them.\n+\n+A meta-commit can have zero or more origin parents. A cherry-pick creates a\n+single origin parent. Certain types of squash merge will create multiple origin\n+parents. Origin parents don't directly cause their origin to become obsolete,\n+but are used when computing blame or locating a merge base. The section\n+on obsolescence over cherry-picks describes how the evolve command uses\n+origin parents.\n+\n+A replaced parent or origin parent may be either a normal commit (indicating\n+the oldest-known version of a change) or another meta-commit (for a change that\n+has already been modified one or more times).\n+\n+The parent-type field needs to go after the committer field since git's rules\n+for forwards-compatibility require that new fields to be at the end of the\n+header. Putting a new field in the middle of the header would break fsck. \n+\n+The presence of an abandoned parent indicates that the change should be pruned\n+by the evolve command, and removed from the repository's history. The abandoned\n+parent points to the version of the change that should be restored if the user\n+attempts to restore the change.\n+\n+Changes\n+-------\n+A branch of meta-commits describes how a commit was produced and what previous\n+commits it is based on. It is also an identifier for a thing the user is\n+currently working on. We refer to such a meta-branch as a change.\n+\n+Local changes are stored in the new refs/metas namespace. Remote changes are\n+stored in the refs/remote/<remotename>/metas namespace.\n+\n+The list of changes in refs/metas is more than just a mechanism for the evolve\n+command to locate obsolete commits. It is also a convenient list of all of a\n+user’s work in progress and their current state - a list of things they’re\n+likely to want to come back to.\n+\n+Strictly speaking, it is the presence of the branch in the refs/metas namespace\n+that marks a branch as being a change, not the fact that it points to a\n+metacommit. Metacommits are only created when a commit is amended or rebased, so\n+in the case where a change points to a commit that has never been modified, the\n+change points to that initial commit rather than a metacommit.\n+\n+Changes are also stored in the refs/hiddenmetas namespace. Hiddenmetas holds\n+metadata for historical changes that are not currently in progress by the user.\n+Commands like filter-branch and other bulk import commands create metadata in\n+this namespace.\n+\n+Note that the changes in hiddenmetas get special treatment in several ways:\n+\n+- They are not cleaned up automatically once merged, since it is expected that\n+  they refer to historical changes.\n+- User commands that modify changes don't append to these changes as they would\n+  to a change in refs/metas.\n+- They are not displayed when the user lists their local changes.\n+\n+Obsolescence\n+------------\n+A commit is considered obsolete if it is reachable from the “replaces” edges\n+anywhere in the history of a change and it isn’t the head of that change.\n+Commits may be the content for 0 or more meta-commits. If the same commit\n+appears in multiple changes, it is not obsolete if it is the head of any of\n+those changes.\n+\n+Note that there is an exeption to this rule. The metas namespace takes\n+precedence over the hiddenmetas namespace for the purpose of obsolescence. That\n+is, if a change appears in a replaces edge of a change in the metas namespace,\n+it is obsolete even if it also appears as the head of a change in the\n+hiddenmetas namespace.\n+\n+This special case prevents the hiddenmetas namespace from creating divergence\n+with the user's work in progress, and allows the user to resolve historical\n+divergence by creating new changes in the metas namespace.\n+\n+Divergence\n+----------\n+From the user’s perspective, two changes are divergent if they both ask for\n+different replacements to the same commit. More precisely, a target commit is\n+considered divergent if there is more than one commit at the head of a change in\n+refs/metas that leads to the target commit via an unbroken chain of “obsolete”\n+parents.\n+\n+Much like a merge conflict, divergence is a situation that requires user\n+intervention to resolve. The evolve command will stop when it encounters\n+divergence and prompt the user to resolve the problem. Users can solve the\n+problem in several ways:\n+\n+- Discard one of the changes (by deleting its change branch).\n+- Merge the two changes (producing a single change branch).\n+- Copy one of the changes (keep both commits, but one of them gets a new\n+  metacommit appended to its history that is connected to its predecessor via an\n+  origin edge rather than an obsolete edge. That new change no longer obsoletes\n+  the original.)\n+\n+Obsolescence across cherry-picks\n+--------------------------------\n+By default the evolve command will treat cherry-picks and squash merges as being\n+completely separate from the original. Further amendments to the original commit\n+will have no effect on the cherry-picked copy. However, this behavior may not be\n+desirable in all circumstances.\n+\n+The evolve command may at some point support an option to look for cases where\n+the source of a cherry-pick or squash merge has itself been amended, and\n+automatically apply that same change to the cherry-picked copy. In such cases,\n+it would traverse origin edges rather than ignoring them, and would treat a\n+commit with origin edges as being obsolete if any of its origins were obsolete.\n+\n+Garbage collection\n+------------------\n+For GC purposes, meta-commits are normal commits. Just as a commit causes its\n+parents and tree to be retained, a meta-commit also causes its parents to be\n+retained.\n+\n+Change creation\n+---------------\n+Changes are created automatically whenever the user runs a command like “commit”\n+that has the semantics of creating a new change. They also move forward\n+automatically even if they’re not checked out. For example, whenever the user\n+runs a command like “commit --amend” that modifies a commit, all branches in\n+refs/metas that pointed to the old commit move forward to point to its\n+replacement instead. This also happens when the user is working from a detached\n+head.\n+\n+This does not mean that every commit has a corresponding change. By default,\n+changes only exist for recent locally-created commits. Users may explicitly pull\n+changes from other users or keep their changes around for a long time, but\n+either behavior requires a user to opt-in. Code review systems like gerrit may\n+also choose to keep changes around forever.\n+\n+Note that the changes in refs/metas serve a dual function as both a way to\n+identify obsolete changes and as a way for the user to keep track of their work\n+in progress. If we were only concerned with identifying obsolete changes, it\n+would be sufficient to create the change branch lazily the first time a commit\n+is obsoleted. Addressing the second use - of refs/metas as a mechanism for\n+keeping track of work in progress - is the reason for eagerly creating the\n+change on first commit.\n+\n+Change naming\n+-------------\n+When a change is first created, the only requirement for its name is that it\n+must be unique. Good names would also serve as useful mnemonics and be easy to\n+type. For example, a short word from the commit message containing no numbers or\n+special characters and that shows up with low frequency in other commit messages\n+would make a good choice.\n+\n+Different users may prefer different heuristics for their change names. For this\n+reason a new hook will be introduced to compute change names. Git will invoke\n+the hook for all newly-created changes and will append a numeric suffix if the\n+name isn’t unique. The default heuristics are not specified by this proposal and\n+may change during implementation.\n+\n+Change deletion\n+---------------\n+Changes are normally only interesting to a user while a commit is still in\n+development and under review. Once the commit has submitted wherever it is\n+going, its change can be discarded.\n+\n+The normal way of deleting changes makes this easy to do - changes are deleted\n+by the evolve command when it detects that the change is present in an upstream\n+branch. It does this in two ways: if the latest commit in a change either shows\n+up in the branch history or the change becomes empty after a rebase, it is\n+considered merged and the change is discarded. In this context, an “upstream\n+branch” is any branch passed in as the upstream argument of the evolve command.\n+\n+In case this sometimes deletes a useful change, such automatic deletions are\n+recorded in the reflog allowing them to be easily recovered.\n+\n+Sharing changes\n+---------------\n+Change histories are shared by pushing or fetching meta-commits and change\n+branches. This provides users with a lot of control of what to share and\n+repository implementations with control over what to retain.\n+\n+Users that only want to share the content of a commit can do so by pushing the\n+commit itself as they currently would. Users that want to share an edit history\n+for the commit can push its change, which would point to a meta-commit rather\n+than the commit itself if there is any history to share. Note that multiple\n+changes can refer to the same commits, so it’s possible to construct and push a\n+different history for the same commit in order to remove sensitive or irrelevant\n+intermediate states.\n+\n+Imagine the user is working on a change “mychange” that is currently the latest\n+commit on master, they have two ways to share it:\n+\n+# User shares just a commit without its history\n+> git push origin master\n+\n+# User shares the full history of the commit to a review system\n+> git push origin metas/mychange:refs/for/master\n+\n+# User fetches a collaborator’s modifications to their change\n+> git fetch remotename metas/mychange\n+# Which updates the ref remote/remotename/metas/mychange\n+\n+This will cause more intermediate states to be shared with the server than would\n+have been shared previously. A review system like gerrit would need to keep\n+track of which states had been explicitly pushed versus other intermediate\n+states in order to de-emphasize (or hide) the extra intermediate states from the\n+user interface.\n+\n+Merge-base\n+----------\n+Merge-base will be changed to search the meta-commit graph for common ancestors\n+as well as the commit graph, and will generally prefer results from the\n+meta-commit graph over the commit graph. Merge-base will consider meta-commits\n+from all changes, and will traverse both origin and obsolete edges.\n+\n+The reason for this is that - when merging two versions of the same commit\n+together - an earlier version of that same commit will usually be much more\n+similar than their common parent. This should make the workflow of collaborating\n+on unsubmitted patches as convenient as the workflow for collaborating in a\n+topic branch by eliminating repeated merges.\n+\n+Configuration\n+-------------\n+The core.enableChanges configuration variable enables the creation and update\n+of change branches. This is enabled by default.\n+\n+User interface\n+--------------\n+All git porcelain commands that create commits are classified as having one of\n+four behaviors: modify, create, copy, or import. These behaviors are discussed\n+in more detail below.\n+\n+Modify commands\n+---------------\n+Modification commands (commit --amend, rebase) will mark the old commit as\n+obsolete by creating a new meta-commit that references the old one as a\n+replaced parent. In the event that multiple changes point to the same commit,\n+this is done independently for every such change.\n+\n+More specifically, modifications work like this:\n+\n+1. Locate all existing changes for which the old commit is the content for the\n+   head of the change branch. If no such branch exists, create one that points\n+   to the old commit. Changes that include this commit in their history but not\n+   at their head are explicitly not included.\n+2. For every such change, create a new meta-commit that references the new\n+   commit as its content and references the old head of the change as a\n+   replaced parent.\n+3. Move the change branch forward to point to the new meta-commit.\n+\n+Copy commands\n+-------------\n+Copy commands (cherry-pick, merge --squash) create a new meta-commit that\n+references the old commits as origin parents. Besides the fact that the new\n+parents are tagged differently, copy commands work the same way as modify\n+commands.\n+\n+Create commands\n+---------------\n+Creation commands (commit, merge) create a new commit and a new change that\n+points to that commit. The do not create any meta-commits.\n+\n+Import commands\n+---------------\n+Import commands (fetch, pull) do not create any new meta-commits or changes\n+unless that is specifically what they are importing. For example, the fetch\n+command would update remote/origin/metas/change35 and fetch all referenced\n+meta-commits if asked to do so directly, but it wouldn’t create any changes or\n+meta-commits for commits discovered on the master branch when running “git fetch\n+origin master”.\n+\n+Other commands\n+--------------\n+Some commands don’t fit cleanly into one of the above categories.\n+\n+Semantically, filter-branch should be treated as a modify command, but doing so\n+is likely to create a lot of irrelevant clutter in the changes namespace and the\n+large number of extra change refs may introduce performance problems. We\n+recommend treating filter-branch as an import command initially, but making it\n+behave more like a modify command in future follow-up work. One possible\n+solution may be to treat commits that are part of existing changes as being\n+modified but to avoid creating changes for other rewritten changes.\n+\n+Once the evolve command can handle obsolescence across cherry-picks, such\n+cherry-picks will result in a hybrid move-and-copy operation. It will create\n+cherry-picks that replace other cherry-picks, which will have both origin edges\n+(pointing to the new source commit being picked) and obsolete edges (pointing to\n+the previous cherry-pick being replaced).\n+\n+Evolve\n+------\n+The evolve command performs the correct sequence of rebases such that no change\n+has an obsolete parent. The syntax looks like this:\n+\n+git evolve [--abort][--continue][--quit] [upstream…]\n+\n+It takes an optional list of upstream branches. All changes whose parent shows\n+up in the history of one of the upstream branches will be rebased onto the\n+upstream branch before resolving obsolete parents.\n+\n+Any change whose latest state is found in an upstream branch (or that ends up\n+empty after rebase) will be deleted. This is the normal mechanism for deleting\n+changes. Changes are created automatically on the first commit, and are deleted\n+automatically when evolve determines that they’ve been merged upstream.\n+\n+Orphan commits are commits with obsolete parents. The evolve command then\n+repeatedly rebases orphan commits with non-orphan parents until there are either\n+no orphan commits left, a merge conflict is discovered, or a divergent parent is\n+discovered.\n+\n+When evolve discovers divergence, it will first check if it can resolve the\n+divergence automatically using one of its enabled transformations. Supported\n+transformations are:\n+\n+- Check if the user has already merged the divergent changes in a follow-up\n+  change. That is, look for an existing merge in a follow-up change where all\n+  the parents are divergent versions of the same change. Squash that merge with\n+  its parents and use the result as the resolution for the divergence.\n+\n+- Attempt to auto-merge all the divergent changes (disabled by default).\n+\n+Each of the transformations can be enabled or disabled by command line options.\n+\n+The --abort option returns all changes to the state they were in prior to\n+invoking evolve, and the --quit option terminates the current evolution without\n+changing the current state.\n+\n+If the working tree is dirty, evolve will attempt to stash the user's changes\n+before applying the evolve and then reapply those changes afterward, in much\n+the same way as rebase --autostash does.\n+\n+Checkout\n+--------\n+Running checkout on a change by name has the same effect as checking out a\n+detached head pointing to the latest commit on that change-branch. There is no\n+need to ever have HEAD point to a change since changes always move forward when\n+necessary, no matter what branch the user has checked out\n+\n+Meta-commits themselves cannot be checked out by their hash.\n+\n+Reset\n+-----\n+Resetting a branch to a change by name is the same as resetting to the commit at\n+that change’s head.\n+\n+Commit\n+------\n+Commit --amend gets modify semantics and will move existing changes forward. The\n+normal form of commit gets create semantics and will create a new change.\n+\n+$ touch foo && git add . && git commit -m \"foo\" && git tag A\n+$ touch bar && git add . && git commit -m \"bar\" && git tag B\n+$ touch baz && git add . && git commit -m \"baz\" && git tag C\n+\n+This produces the following commits:\n+A(tree=[foo])\n+B(tree=[foo, bar], parent=A)\n+C(tree=[foo, bar, baz], parent=B)\n+\n+...along with three changes:\n+metas/foo = A\n+metas/bar = B\n+metas/baz = C\n+\n+Running commit --amend does the following:\n+$ git checkout B\n+$ touch zoom && git add . && git commit --amend -m \"baz and zoom\"\n+$ git tag D\n+\n+Commits:\n+A(tree=[foo])\n+B(tree=[foo, bar], parent=A)\n+C(tree=[foo, bar, baz], parent=B)\n+D(tree=[foo, bar, zoom], parent=A)\n+Dmeta(content=D, obsolete=B)\n+\n+Changes:\n+metas/foo = A\n+metas/bar = Dmeta\n+metas/baz = C\n+\n+Merge\n+-----\n+Merge gets create, modify, or copy semantics based on what is being merged and\n+the options being used.\n+\n+The --squash version of merge gets copy semantics (it produces a new change that\n+is marked as a copy of all the original changes that were squashed into it).\n+\n+The “modify” version of merge replaces both of the original commits with the\n+resulting merge commit. This is one of the standard mechanisms for resolving\n+divergence. The parents of the merge commit are the parents of the two commits\n+being merged. The resulting commit will not be a merge commit if both of the\n+original commits had the same parent or if one was the parent of the other.\n+\n+The “create” version of merge creates a new change pointing to a merge commit\n+that has both original commits as parents. The result is what merge produces now\n+- a new merge commit. However, this version of merge doesn’t directly resolve\n+divergence.\n+\n+To select between these two behaviors, merge gets new “--amend” and “--noamend”\n+options which select between the “create” and “modify” behaviors respectively,\n+with noamend being the default.\n+\n+For example, imagine we created two divergent changes like this:\n+\n+$ touch foo && git add . && git commit -m \"foo\" && git tag A\n+$ touch bar && git add . && git commit -m \"bar\" && git tag B\n+$ touch baz && git add . && git commit --amend -m \"bar and baz\"\n+$ git tag C\n+$ git checkout B\n+$ touch bam && git add . && git commit --amend -m \"bar and bam\"\n+$ git tag D\n+\n+At this point the commit graph looks like this:\n+\n+A(tree=[foo])\n+B(tree=[bar], parent=A)\n+C(tree=[bar, baz], parent=A)\n+D(tree=[bar, bam], parent=A)\n+Cmeta(content=C, obsoletes=B)\n+Dmeta(content=D, obsoletes=B)\n+\n+There would be three active changes with heads pointing as follows:\n+\n+metas/changeA=A\n+metas/changeB=Cmeta\n+metas/changeB2=Dmeta\n+\n+ChangeB and changeB2 are divergent at this point. Lets consider what happens if\n+perform each type of merge between changeB and changeB2.\n+\n+Merge example: Amend merge\n+One way to resolve divergent changes is to use an amend merge. Recall that HEAD\n+is currently pointing to D at this point.\n+\n+$ git merge --amend metas/changeB\n+\n+Here we’ve asked for an amend merge since we’re trying to resolve divergence\n+between two versions of the same change. There are no conflicts so we end up\n+with this:\n+\n+E(tree=[bar, baz, bam], parent=A)\n+Emeta(content=E, obsoletes=[Cmeta, Dmeta])\n+\n+With the following branches:\n+\n+metas/changeA=A\n+metas/changeB=Emeta\n+metas/changeB2=Emeta\n+\n+Notice that the result of the “amend merge” is a replacement for C and D rather\n+than a new commit with C and D as parents (as a normal merge would have\n+produced). The parents of the amend merge are the parents of C and D which - in\n+this case - is just A, so the result is not a merge commit. Also notice that\n+changeB and changeB2 are now aliases for the same change.\n+\n+Merge example: Noamend merge\n+Consider what would have happened if we’d used a noamend merge instead. Recall\n+that HEAD was at D and our branches looked like this:\n+\n+metas/changeA=A\n+metas/changeB=Cmeta\n+metas/changeB2=Dmeta\n+\n+$ git merge --noamend metas/changeB\n+\n+That would produce the sort of merge we’d normally expect today:\n+\n+F(tree=[bar, baz, bam], parent=[C, D])\n+\n+And our changes would look like this:\n+metas/changeA=A\n+metas/changeB=Cmeta\n+metas/changeB2=Dmeta\n+metas/changeF=F\n+\n+In this case, changeB and changeB2 are still divergent and we’ve created a new\n+change for our merge commit. However, this is just a temporary state. The next\n+time we run the “evolve” command, it will discover the divergence but also\n+discover the merge commit F that resolves it. Evolve will suggest converting F\n+into an amend merge in order to resolve the divergence and will display the\n+command for doing so.\n+\n+Rebase\n+------\n+In general the rebase command is treated as a modify command. When a change is\n+rebased, the new commit replaces the original.\n+\n+Rebase --abort is special. Its intent is to restore git to the state it had\n+prior to running rebase. It should move back any changes to point to the refs\n+they had prior to running rebase and delete any new changes that were created as\n+part of the rebase. To achieve this, rebase will save the state of all changes\n+in refs/metas prior to running rebase and will restore the entire namespace\n+after rebase completes (deleting any newly-created changes). Newly-created\n+metacommits are left in place, but will have no effect until garbage collected\n+since metacommits are only used if they are reachable from refs/metas.\n+\n+Change\n+------\n+The “change” command can be used to list, rename, reset or delete change. It has\n+a number of subcommands.\n+\n+The \"list\" subcommand lists local changes. If given the -r argument, it lists\n+remote changes.\n+\n+The \"rename\" subcommand renames a change, given its old and new name. If the old\n+name is omitted and there is exactly one change pointing to the current HEAD,\n+that change is renamed. If there are no changes pointing to the current HEAD,\n+one is created with the given name.\n+\n+The \"forget\" subcommand deletes a change by deleting its ref from the metas/\n+namespace. This is the normal way to delete extra aliases for a change if the\n+change has more than one name. By default, this will refuse to delete the last\n+alias for a change if there are any other changes that reference this change as\n+a parent.\n+\n+The \"update\" subcommand adds a new state to a change. It uses the default\n+algorithm for assigning change names. If the content commit is omitted, HEAD is\n+used. If given the optional --force argument, it will overwrite any existing\n+change of the same name. This latter form of \"update\" can be used to effectively\n+reset changes.\n+\n+The \"update\" command can accept any number of --origin and --replace arguments.\n+If any are present, the resulting change branch will point to a metacommit\n+containing the given origin and replacement edges.\n+\n+The \"replace\" command records a replacement in the obsolescence graph, given a\n+list of obsolete commits or metacommits followed by their replacement. This\n+behaves like a normal \"modify\" command, except that the replacement is an\n+existing commit. If an obsolete commit points to a metacommit, only a change\n+branch pointing to exactly that metacommit moves forward. If an obsolete commit\n+points to a normal commit, all change branches pointing to that commit move\n+forward. If no change branches moved forward, a new change branch is created\n+using the default name.\n+\n+The \"abandon\" command deletes a change using obsolescence markers. It marks the\n+change as being obsolete and having been replaced by its parent. If given no\n+arguments, it applies to the current commit. Running evolve will cause any\n+abandoned changes to be removed from the branch. Any child changes will be\n+reparented on top of the parent of the abandoned change. If the current change\n+is abandoned, HEAD will move to point to its parent.\n+\n+The \"restore\" command restores a previously-abandoned change.\n+\n+The \"prune\" command deletes all obsolete changes and all changes that are\n+present in the given branch. Note that such changes can be recovered from the\n+reflog.\n+\n+Combined with the GC protection that is offered, this is intended to facilitate\n+a workflow that relies on changes instead of branches. Users could choose to\n+work with no local branches and use changes instead - both for mailing list and\n+gerrit workflows.\n+\n+Log\n+---\n+When a commit is shown in git log that is part of a change, it is decorated with\n+extra change information. If it is the head of a change, the name of the change\n+is shown next to the list of branches. If it is obsolete, it is decorated with\n+the text “obsolete, <n> commits behind <changename>”.\n+\n+Log gets a new --obslog argument indicating that the obsolescence graph should\n+be followed instead of the commit graph. This also changes the default\n+formatting options to make them more appropriate for viewing different\n+iterations of the same commit.\n+\n+Pull\n+----\n+\n+Pull gets an --evolve argument that will automatically attempt to run \"evolve\"\n+on any affected branches after pulling.\n+\n+We also introduce an \"evolve\" enum value for the branch.<name>.rebase config\n+value. When set, the evolve behavior will happen automatically for that branch\n+after every pull even if the --evolve argument is not used.\n+\n+Next\n+----\n+\n+The \"next\" command will reset HEAD to a non-obsolete commit that refers to this\n+change as its parent. If there is more than one such change, the user will be\n+prompted. If given the --evolve argument, the next commit will be evolved if\n+necessary first.\n+\n+The \"next\" command can be thought of as the opposite of\n+\"git reset --hard HEAD^\" in that it navigates to a child commit rather than a\n+parent.\n+\n+Other options considered\n+========================\n+We considered several other options for storing the obsolescence graph. This\n+section describes the other options and why they were rejected.\n+\n+Commit header\n+-------------\n+Add an “obsoletes” field to the commit header that points backwards from a\n+commit to the previous commits it obsoletes.\n+\n+Pros:\n+- Very simple\n+- Easy to traverse from a commit to the previous commits it obsoletes.\n+Cons:\n+- Adds a cost to the storage format, even for commits where the change history\n+  is uninteresting.\n+- Unconditionally prevents the change history from being garbage collected.\n+- Always causes the change history to be shared when pushing or pulling changes.\n+\n+Git notes\n+---------\n+Instead of storing obsolescence information in metacommits, the metacommit\n+content could go in a new notes namespace - say refs/notes/metacommit. Each note\n+would contain the list of obsolete and origin parents, and an automerger could\n+be supplied to make it easy to merge the metacommit notes from different remotes.\n+\n+Pros:\n+- Easy to locate all commits obsoleted by a given commit (since there would only\n+  be one metacommit for any given commit).\n+Cons:\n+- Wrong GC behavior (obsolete commits wouldn’t automatically be retained by GC)\n+  unless we introduced a special case for these kinds of notes.\n+- No way to selectively share or pull the metacommits for one specific change.\n+  It would be all-or-nothing, which would be expensive. This could be addressed\n+  by changes to the protocol, but this would be invasive.\n+- Requires custom auto-merging behavior on fetch.\n+\n+Tags\n+----\n+Put the content of the metacommit in a message attached to tag on the\n+replacement commit. This is very similar to the git notes approach and has the\n+same pros and cons.\n+\n+Simple forward references\n+-------------------------\n+Record an edge from an obsolete commit to its replacement in this form:\n+\n+refs/obsoletes/<A>\n+\n+pointing to commit <B> as an indication that B is the replacement for the\n+obsolete commit A.\n+\n+Pros:\n+- Protects <B> from being garbage collected.\n+- Fast lookup for the evolve operation, without additional search structures\n+  (“what is the replacement for <A>?” is very fast).\n+\n+Cons:\n+- Can’t represent divergence (which is a P0 requirement).\n+- Creates lots of refs (which can be inefficient)\n+- Doesn’t provide a way to fetch only refs for a specific change.\n+- The obslog command requires a search of all refs.\n+\n+Complex forward references\n+--------------------------\n+Record an edge from an obsolete commit to its replacement in this form:\n+\n+refs/obsoletes/<change_id>/obs<A>_<B>\n+\n+Pointing to commit <B> as an indication that B is the replacement for obsolete\n+commit A.\n+\n+Pros:\n+- Permits sharing and fetching refs for only a specific change.\n+- Supports divergence\n+- Protects <B> from being garbage collected.\n+\n+Cons:\n+- Creates lots of refs, which is inefficient.\n+- Doesn’t provide a good lookup structure for lookups in either direction.\n+\n+Backward references\n+-------------------\n+Record an edge from a replacement commit to the obsolete one in this form:\n+\n+refs/obsolescences/<B>\n+\n+Cons:\n+- Doesn’t provide a way to resolve divergence (which is a P0 requirement).\n+- Doesn’t protect <B> from being garbage collected (which could be fixed by\n+  combining this with a refs/metas namespace, as in the metacommit variant).\n+\n+Obsolescences file\n+------------------\n+Create a custom file (or files) in .git recording obsolescences.\n+\n+Pros:\n+- Can store exactly the information we want with exactly the performance we want\n+  for all operations. For example, there could be a disk-based hashtable\n+  permitting constant time lookups in either direction.\n+\n+Cons:\n+- Handling GC, pushing, and pulling would all require custom solutions. GC\n+  issues could be addressed with a repository format extension.\n+\n+Squash points\n+-------------\n+We create and update change branches in refs/metas them at the same time we\n+would in the metacommit proposal. However, rather than pointing to a metacommit\n+branch they point to normal commits and are treated as “squash points” - markers\n+for sequences of commits intended to be squashed together on submission.\n+\n+Amends and rebases work differently than they do now. Rather than actually\n+containing the desired state of a commit, they contain a delta from the previous\n+version along with a squash point indicating that the preceding changes are\n+intended to be squashed on submission. Specifically, amends would become new\n+changes and rebases would become merge commits with the old commit and new\n+parent as parents.\n+\n+When the changes are finally submitted, the squashes are executed, producing the\n+final version of the commit.\n+\n+In addition to the squash points, git would maintain a set of “nosquash” tags\n+for commits that were used as ancestors of a change that are not meant to be\n+included in the squash.\n+\n+For example, if we have this commit graph:\n+\n+A(...)\n+B(parent=A)\n+C(parent=B)\n+\n+...and we amend B to produce D, we’d get:\n+\n+A(...)\n+B(parent=A)\n+C(parent=B)\n+D(parent=B)\n+\n+...along with a new change branch indicating D should be squashed with its\n+parents when submitted:\n+\n+metas/changeB = D\n+metas/changeC = C\n+\n+We’d also create a nosquash tag for A indicating that A shouldn’t be included\n+when changeB is squashed.\n+\n+If a user amends the change again, they’d get:\n+\n+A(...)\n+B(parent=A)\n+C(parent=B)\n+D(parent=B)\n+E(parent=D)\n+\n+metas/changeB = E\n+metas/changeC = C\n+\n+Pros:\n+- Good GC behavior.\n+- Provides a natural way to share changes (they’re just normal branches).\n+- Merge-base works automatically without special cases.\n+- Rewriting the obslog would be easy using existing git commands.\n+- No new data types needed.\n+Cons:\n+- No way to connect the squashed version of a change to the original, so no way\n+  to automatically clean up old changes. This also means users lose all benefits\n+  of the evolve command if they prematurely squash their commits. This may occur\n+  if a user thinks a change is ready for submission, squashes it, and then later\n+  discovers an additional change to make.\n+- Histories would look very cluttered (users would see all previous edits to\n+  their commit in the commit log, and all previous rebases would show up as\n+  merges). Could be quite hard for users to tell what is going on. (Possible\n+  fix: also implement a new smart log feature that displays the log as though\n+  the squashes had occurred).\n+- Need to change the current behavior of current commands (like amend and\n+  rebase) in ways that will be unexpected to many users.\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368288","messageId":"20190201030925.177124-2-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 2/8] sha1-array: implement oid_array_readonly_contains","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:19Z","receivedAt":"2019-02-01T03:09:39Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nImplement a \"readonly_contains\" function for oid_array that won't\nsort the array if it is unsorted. This can be used to test containment in\nthe rare situations where the array order matters.\n\nThe function has intentionally been given a name that is more cumbersome\nthan the \"lookup\" function, which is what most callers will will want\nin most situations.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n sha1-array.c               | 13 +++++++++++++\n sha1-array.h               |  2 ++\n t/helper/test-sha1-array.c |  6 ++++++\n t/t0064-sha1-array.sh      | 22 ++++++++++++++++++++++\n 4 files changed, 43 insertions(+)\n\ndiff --git a/sha1-array.c b/sha1-array.c\nindex d922e94e3f..d1adcbfc8a 100644\n--- a/sha1-array.c\n+++ b/sha1-array.c\n@@ -26,6 +26,19 @@ static const unsigned char *sha1_access(size_t index, void *table)\n \treturn array[index].hash;\n }\n \n+int oid_array_readonly_contains(const struct oid_array* array,\n+\tconst struct object_id* oid)\n+{\n+\tint i;\n+\n+\tif (array->sorted)\n+\t\treturn sha1_pos(oid->hash, array->oid, array->nr, sha1_access) >= 0;\n+\tfor (i = 0; i < array->nr; i++)\n+\t\tif (oideq(&array->oid[i], oid) == 0)\n+\t\t\treturn 1;\n+\treturn 0;\n+}\n+\n int oid_array_lookup(struct oid_array *array, const struct object_id *oid)\n {\n \tif (!array->sorted)\ndiff --git a/sha1-array.h b/sha1-array.h\nindex 55d016c4bf..06d3ad2994 100644\n--- a/sha1-array.h\n+++ b/sha1-array.h\n@@ -13,6 +13,8 @@ struct oid_array {\n void oid_array_append(struct oid_array *array, const struct object_id *oid);\n int oid_array_lookup(struct oid_array *array, const struct object_id *oid);\n void oid_array_clear(struct oid_array *array);\n+int oid_array_readonly_contains(const struct oid_array* array,\n+\tconst struct object_id* oid);\n \n typedef int (*for_each_oid_fn)(const struct object_id *oid,\n \t\t\t       void *data);\ndiff --git a/t/helper/test-sha1-array.c b/t/helper/test-sha1-array.c\nindex ad5e69f9d3..fefb1c984f 100644\n--- a/t/helper/test-sha1-array.c\n+++ b/t/helper/test-sha1-array.c\n@@ -25,10 +25,16 @@ int cmd__sha1_array(int argc, const char **argv)\n \t\t\tif (get_oid_hex(arg, &oid))\n \t\t\t\tdie(\"not a hexadecimal SHA1: %s\", arg);\n \t\t\tprintf(\"%d\\n\", oid_array_lookup(&array, &oid));\n+\t\t} else if (skip_prefix(line.buf, \"readonly_contains \", &arg)) {\n+\t\t\tif (get_oid_hex(arg, &oid))\n+\t\t\t\tdie(\"not a hexadecimal SHA1: %s\", arg);\n+\t\t\tprintf(\"%d\\n\", oid_array_readonly_contains(&array, &oid));\n \t\t} else if (!strcmp(line.buf, \"clear\"))\n \t\t\toid_array_clear(&array);\n \t\telse if (!strcmp(line.buf, \"for_each_unique\"))\n \t\t\toid_array_for_each_unique(&array, print_oid, NULL);\n+\t\telse if (!strcmp(line.buf, \"for_each\"))\n+\t\t\toid_array_for_each(&array, print_oid, NULL);\n \t\telse\n \t\t\tdie(\"unknown command: %s\", line.buf);\n \t}\ndiff --git a/t/t0064-sha1-array.sh b/t/t0064-sha1-array.sh\nindex 5dda570b9a..512dd0f56b 100755\n--- a/t/t0064-sha1-array.sh\n+++ b/t/t0064-sha1-array.sh\n@@ -32,6 +32,28 @@ test_expect_success 'ordered enumeration with duplicate suppression' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'readonly_contains finds existing' '\n+\techo 1 >expect &&\n+\techoid \"\" 88 44 aa 55 >>expect &&\n+\t{\n+\t\techoid append 88 44 aa 55 &&\n+\t\techoid readonly_contains 55 &&\n+\t\techo for_each\n+\t} | test-tool sha1-array >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'readonly_contains non-existing query' '\n+\techo 0 >expect &&\n+\techoid \"\" 88 44 aa 55 >>expect &&\n+\t{\n+\t\techoid append 88 44 aa 55 &&\n+\t\techoid readonly_contains 33 &&\n+\t\techo for_each\n+\t} | test-tool sha1-array >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'lookup' '\n \t{\n \t\techoid append 88 44 aa 55 &&\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368289","messageId":"20190201030925.177124-3-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 3/8] ref-filter: add the metas namespace to ref-filter","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:20Z","receivedAt":"2019-02-01T03:09:41Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nThe metas namespace will contain refs for changes in progress. Add\nsupport for searching this namespace.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n ref-filter.c | 8 ++++++--\n ref-filter.h | 5 +++--\n 2 files changed, 9 insertions(+), 4 deletions(-)\n\ndiff --git a/ref-filter.c b/ref-filter.c\nindex 422a9c9ae3..4d7bd06880 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -1925,7 +1925,8 @@ static int ref_kind_from_refname(const char *refname)\n \t} ref_kind[] = {\n \t\t{ \"refs/heads/\" , FILTER_REFS_BRANCHES },\n \t\t{ \"refs/remotes/\" , FILTER_REFS_REMOTES },\n-\t\t{ \"refs/tags/\", FILTER_REFS_TAGS}\n+\t\t{ \"refs/tags/\", FILTER_REFS_TAGS },\n+\t\t{ \"refs/metas/\", FILTER_REFS_CHANGES }\n \t};\n \n \tif (!strcmp(refname, \"HEAD\"))\n@@ -1943,7 +1944,8 @@ static int filter_ref_kind(struct ref_filter *filter, const char *refname)\n {\n \tif (filter->kind == FILTER_REFS_BRANCHES ||\n \t    filter->kind == FILTER_REFS_REMOTES ||\n-\t    filter->kind == FILTER_REFS_TAGS)\n+\t    filter->kind == FILTER_REFS_TAGS ||\n+\t    filter->kind == FILTER_REFS_CHANGES )\n \t\treturn filter->kind;\n \treturn ref_kind_from_refname(refname);\n }\n@@ -2128,6 +2130,8 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int\n \t\t\tret = for_each_fullref_in(\"refs/remotes/\", ref_filter_handler, &ref_cbdata, broken);\n \t\telse if (filter->kind == FILTER_REFS_TAGS)\n \t\t\tret = for_each_fullref_in(\"refs/tags/\", ref_filter_handler, &ref_cbdata, broken);\n+\t\telse if (filter->kind == FILTER_REFS_CHANGES)\n+\t\t\tret = for_each_fullref_in(\"refs/metas/\", ref_filter_handler, &ref_cbdata, broken);\n \t\telse if (filter->kind & FILTER_REFS_ALL)\n \t\t\tret = for_each_fullref_in_pattern(filter, ref_filter_handler, &ref_cbdata, broken);\n \t\tif (!ret && (filter->kind & FILTER_REFS_DETACHED_HEAD))\ndiff --git a/ref-filter.h b/ref-filter.h\nindex 85c8ebc3b9..19a3e57845 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -18,9 +18,10 @@\n #define FILTER_REFS_BRANCHES       0x0004\n #define FILTER_REFS_REMOTES        0x0008\n #define FILTER_REFS_OTHERS         0x0010\n-#define FILTER_REFS_ALL            (FILTER_REFS_TAGS | FILTER_REFS_BRANCHES | \\\n-\t\t\t\t    FILTER_REFS_REMOTES | FILTER_REFS_OTHERS)\n #define FILTER_REFS_DETACHED_HEAD  0x0020\n+#define FILTER_REFS_CHANGES        0X0040\n+#define FILTER_REFS_ALL            (FILTER_REFS_TAGS | FILTER_REFS_BRANCHES | \\\n+\t\t\t\t    FILTER_REFS_REMOTES | FILTER_REFS_CHANGES | FILTER_REFS_OTHERS)\n #define FILTER_REFS_KIND_MASK      (FILTER_REFS_ALL | FILTER_REFS_DETACHED_HEAD)\n \n struct atom_value;\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368290","messageId":"20190201030925.177124-4-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 4/8] evolve: add support for parsing metacommits","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:21Z","receivedAt":"2019-02-01T03:09:43Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nThis patch adds the get_metacommit_content method, which can classify\ncommits as either metacommits or normal commits, determine whether they\nare abandoned, and extract the content commit's object id from the\nmetacommit.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n Makefile            |   1 +\n metacommit-parser.c | 110 ++++++++++++++++++++++++++++++++++++++++++++\n metacommit-parser.h |  16 +++++++\n 3 files changed, 127 insertions(+)\n create mode 100644 metacommit-parser.c\n create mode 100644 metacommit-parser.h\n\ndiff --git a/Makefile b/Makefile\nindex 6e8d017e8e..c850a407ec 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -928,6 +928,7 @@ LIB_OBJS += merge.o\n LIB_OBJS += merge-blobs.o\n LIB_OBJS += merge-recursive.o\n LIB_OBJS += mergesort.o\n+LIB_OBJS += metacommit-parser.o\n LIB_OBJS += midx.o\n LIB_OBJS += name-hash.o\n LIB_OBJS += negotiator/default.o\ndiff --git a/metacommit-parser.c b/metacommit-parser.c\nnew file mode 100644\nindex 0000000000..70c1428bfc\n--- /dev/null\n+++ b/metacommit-parser.c\n@@ -0,0 +1,110 @@\n+#include \"cache.h\"\n+#include \"metacommit-parser.h\"\n+#include \"commit.h\"\n+\n+/*\n+ * Search the commit buffer for a line starting with the given key. Unlike\n+ * find_commit_header, this also searches the commit message body.\n+ */\n+static const char *find_key(const char *msg, const char *key, size_t *out_len)\n+{\n+\tint key_len = strlen(key);\n+\tconst char *line = msg;\n+\n+\twhile (line) {\n+\t\tconst char *eol = strchrnul(line, '\\n');\n+\n+\t\tif (eol - line > key_len && !memcmp(line, key, key_len) &&\n+\t\t    line[key_len] == ' ') {\n+\t\t\t*out_len = eol - line - key_len - 1;\n+\t\t\treturn line + key_len + 1;\n+\t\t}\n+\t\tline = *eol ? eol + 1 : NULL;\n+\t}\n+\treturn NULL;\n+}\n+\n+static struct commit *get_commit_by_index(struct commit_list *to_search, int index)\n+{\n+\twhile (to_search && index) {\n+\t\tto_search = to_search->next;\n+\t\tindex--;\n+\t}\n+\n+\tif (!to_search)\n+\t\treturn NULL;\n+\n+\treturn to_search->item;\n+}\n+\n+/*\n+ * Writes the index of the content parent to \"result\". Returns the metacommit\n+ * type. See the METACOMMIT_TYPE_* constants.\n+ */\n+static int index_of_content_commit(const char *buffer, int *result)\n+{\n+\tint index = 0;\n+\tint ret = METACOMMIT_TYPE_NONE;\n+\tsize_t parent_types_size;\n+\tconst char *parent_types = find_key(buffer, \"parent-type\",\n+\t\t&parent_types_size);\n+\tconst char *end;\n+\tconst char *enum_start = parent_types;\n+\tint enum_length = 0;\n+\n+\tif (!parent_types)\n+\t\treturn METACOMMIT_TYPE_NONE;\n+\n+\tend = &parent_types[parent_types_size];\n+\n+\twhile (1) {\n+\t\tchar next = *parent_types;\n+\t\tif (next == ' ' || parent_types >= end) {\n+\t\t\tif (enum_length == 1) {\n+\t\t\t\tchar first_char_in_enum = *enum_start;\n+\t\t\t\tif (first_char_in_enum == 'c') {\n+\t\t\t\t\tret = METACOMMIT_TYPE_NORMAL;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t\tif (first_char_in_enum == 'a') {\n+\t\t\t\t\tret = METACOMMIT_TYPE_ABANDONED;\n+\t\t\t\t\tbreak;\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif (parent_types >= end)\n+\t\t\t\treturn METACOMMIT_TYPE_NONE;\n+\t\t\tenum_start = parent_types + 1;\n+\t\t\tenum_length = 0;\n+\t\t\tindex++;\n+\t\t} else {\n+\t\t\tenum_length++;\n+\t\t}\n+\t\tparent_types++;\n+\t}\n+\n+\t*result = index;\n+\treturn ret;\n+}\n+\n+/*\n+ * Writes the content parent's object id to \"content\".\n+ * Returns the metacommit type. See the METACOMMIT_TYPE_* constants.\n+ */\n+int get_metacommit_content(struct commit *commit, struct object_id *content)\n+{\n+\tconst char *buffer = get_commit_buffer(commit, NULL);\n+\tint index = 0;\n+\tint ret = index_of_content_commit(buffer, &index);\n+\tstruct commit *content_parent;\n+\n+\tif (ret == METACOMMIT_TYPE_NONE)\n+\t\treturn ret;\n+\n+\tcontent_parent = get_commit_by_index(commit->parents, index);\n+\n+\tif (!content_parent)\n+\t\treturn METACOMMIT_TYPE_NONE;\n+\n+\toidcpy(content, &(content_parent->object.oid));\n+\treturn ret;\n+}\ndiff --git a/metacommit-parser.h b/metacommit-parser.h\nnew file mode 100644\nindex 0000000000..1fad17dbf8\n--- /dev/null\n+++ b/metacommit-parser.h\n@@ -0,0 +1,16 @@\n+#ifndef METACOMMIT_PARSER_H\n+#define METACOMMIT_PARSER_H\n+\n+/* Indicates a normal commit (non-metacommit) */\n+#define METACOMMIT_TYPE_NONE 0\n+/* Indicates a metacommit with normal content (non-abandoned) */\n+#define METACOMMIT_TYPE_NORMAL 1\n+/* Indicates a metacommit with abandoned content */\n+#define METACOMMIT_TYPE_ABANDONED 2\n+\n+struct commit;\n+\n+extern int get_metacommit_content(\n+\tstruct commit *commit, struct object_id *content);\n+\n+#endif\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368291","messageId":"20190201030925.177124-5-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 5/8] evolve: add the change-table structure","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:22Z","receivedAt":"2019-02-01T03:09:46Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nA change table stores a list of changes, and supports efficient lookup\nfrom a commit hash to the list of changes that reference that commit\ndirectly.\n\nIt can be used to look up content commits or metacommits at the head\nof a change, but does not support lookup of commits referenced as part\nof the commit history.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n Makefile       |   1 +\n change-table.c | 176 +++++++++++++++++++++++++++++++++++++++++++++++++\n change-table.h | 127 +++++++++++++++++++++++++++++++++++\n 3 files changed, 304 insertions(+)\n create mode 100644 change-table.c\n create mode 100644 change-table.h\n\ndiff --git a/Makefile b/Makefile\nindex c850a407ec..2e35e1421d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -853,6 +853,7 @@ LIB_OBJS += branch.o\n LIB_OBJS += bulk-checkin.o\n LIB_OBJS += bundle.o\n LIB_OBJS += cache-tree.o\n+LIB_OBJS += change-table.o\n LIB_OBJS += chdir-notify.o\n LIB_OBJS += checkout.o\n LIB_OBJS += color.o\ndiff --git a/change-table.c b/change-table.c\nnew file mode 100644\nindex 0000000000..15105b2d96\n--- /dev/null\n+++ b/change-table.c\n@@ -0,0 +1,176 @@\n+#include \"cache.h\"\n+#include \"change-table.h\"\n+#include \"commit.h\"\n+#include \"ref-filter.h\"\n+#include \"metacommit-parser.h\"\n+\n+void change_table_init(struct change_table *to_initialize)\n+{\n+\tmemset(to_initialize, 0, sizeof(*to_initialize));\n+\tmem_pool_init(&to_initialize->memory_pool, 0);\n+\tto_initialize->memory_pool->block_alloc = 4*1024 - sizeof(struct mp_block);\n+\toidmap_init(&to_initialize->oid_to_metadata_index, 0);\n+\tstring_list_init(&to_initialize->refname_to_change_head, 1);\n+}\n+\n+static void change_list_clear(struct change_list *to_clear) {\n+\tstring_list_clear(&to_clear->additional_refnames, 0);\n+}\n+\n+static void commit_change_list_entry_clear(\n+\tstruct commit_change_list_entry *to_clear) {\n+\tchange_list_clear(&to_clear->changes);\n+}\n+\n+void change_table_clear(struct change_table *to_clear)\n+{\n+\tstruct oidmap_iter iter;\n+\tstruct commit_change_list_entry *next;\n+\tfor (next = oidmap_iter_first(&to_clear->oid_to_metadata_index, &iter);\n+\t\tnext;\n+\t\tnext = oidmap_iter_next(&iter)) {\n+\n+\t\tcommit_change_list_entry_clear(next);\n+\t}\n+\n+\toidmap_free(&to_clear->oid_to_metadata_index, 0);\n+\tstring_list_clear(&to_clear->refname_to_change_head, 0);\n+\tmem_pool_discard(to_clear->memory_pool, 0);\n+}\n+\n+static void add_head_to_commit(struct change_table *to_modify,\n+\tconst struct object_id *to_add, const char *refname)\n+{\n+\tstruct commit_change_list_entry *entry;\n+\n+\t/* Note: the indices in the map are 1-based. 0 is used to indicate a missing\n+\t * element. */\n+\tentry = oidmap_get(&to_modify->oid_to_metadata_index, to_add);\n+\tif (!entry) {\n+\t\tentry = mem_pool_calloc(to_modify->memory_pool, 1,\n+\t\t\tsizeof(*entry));\n+\t\toidcpy(&entry->entry.oid, to_add);\n+\t\toidmap_put(&to_modify->oid_to_metadata_index, entry);\n+\t\tstring_list_init(&entry->changes.additional_refnames, 0);\n+\t}\n+\n+\tif (entry->changes.first_refname == NULL)\n+\t\tentry->changes.first_refname = refname;\n+\telse\n+\t\tstring_list_insert(&entry->changes.additional_refnames, refname);\n+}\n+\n+void change_table_add(struct change_table *to_modify, const char *refname,\n+\tstruct commit *to_add)\n+{\n+\tstruct change_head *new_head;\n+\tstruct string_list_item *new_item;\n+\tint metacommit_type;\n+\n+\tnew_head = mem_pool_calloc(to_modify->memory_pool, 1,\n+\t\tsizeof(*new_head));\n+\n+\toidcpy(&new_head->head, &to_add->object.oid);\n+\n+\tmetacommit_type = get_metacommit_content(to_add, &new_head->content);\n+\tif (metacommit_type == METACOMMIT_TYPE_NONE)\n+\t\toidcpy(&new_head->content, &to_add->object.oid);\n+\tnew_head->abandoned = (metacommit_type == METACOMMIT_TYPE_ABANDONED);\n+\tnew_head->remote = starts_with(refname, \"refs/remote/\");\n+\tnew_head->hidden = starts_with(refname, \"refs/hiddenmetas/\");\n+\n+\tnew_item = string_list_insert(&to_modify->refname_to_change_head, refname);\n+\tnew_item->util = new_head;\n+\t/* Use pointers to the copy of the string we're retaining locally */\n+\trefname = new_item->string;\n+\n+\tif (!oideq(&new_head->content, &new_head->head))\n+\t\tadd_head_to_commit(to_modify, &new_head->content, refname);\n+\tadd_head_to_commit(to_modify, &new_head->head, refname);\n+}\n+\n+void change_table_add_all_visible(struct change_table *to_modify,\n+\tstruct repository* repo)\n+{\n+\tstruct ref_filter filter;\n+\tconst char *name_patterns[] = {NULL};\n+\tmemset(&filter, 0, sizeof(filter));\n+\tfilter.kind = FILTER_REFS_CHANGES;\n+\tfilter.name_patterns = name_patterns;\n+\n+\tchange_table_add_matching_filter(to_modify, repo, &filter);\n+}\n+\n+void change_table_add_matching_filter(struct change_table *to_modify,\n+\tstruct repository* repo, struct ref_filter *filter)\n+{\n+\tstruct ref_array matching_refs;\n+\tint i;\n+\n+\tmemset(&matching_refs, 0, sizeof(matching_refs));\n+\tfilter_refs(&matching_refs, filter, filter->kind);\n+\n+\t/* Determine the object id for the latest content commit for each change.\n+\t * Fetch the commit at the head of each change ref. If it's a normal commit,\n+\t * that's the commit we want. If it's a metacommit, locate its content parent\n+\t * and use that. */\n+\n+\tfor (i = 0; i < matching_refs.nr; i++) {\n+\t\tstruct ref_array_item *item = matching_refs.items[i];\n+\t\tstruct commit *commit = item->commit;\n+\n+\t\tcommit = lookup_commit_reference_gently(repo, &item->objectname, 1);\n+\n+\t\tif (commit != NULL)\n+\t\t\tchange_table_add(to_modify, item->refname, commit);\n+\t}\n+\n+\tref_array_clear(&matching_refs);\n+}\n+\n+static int return_true_callback(const char *refname, void *cb_data)\n+{\n+\treturn 1;\n+}\n+\n+int change_table_has_change_referencing(struct change_table *changes,\n+\tconst struct object_id *referenced_commit_id)\n+{\n+\treturn for_each_change_referencing(changes, referenced_commit_id,\n+\t\treturn_true_callback, NULL);\n+}\n+\n+int for_each_change_referencing(struct change_table *table,\n+\tconst struct object_id *referenced_commit_id, each_change_fn fn, void *cb_data)\n+{\n+\tconst struct change_list *changes;\n+\tint i;\n+\tint retvalue;\n+\tstruct commit_change_list_entry *entry;\n+\n+\tentry = oidmap_get(&table->oid_to_metadata_index,\n+\t\treferenced_commit_id);\n+\t/* If this commit isn't referenced by any changes, it won't be in the map */\n+\tif (!entry)\n+\t\treturn 0;\n+\tchanges = &entry->changes;\n+\tif (changes->first_refname == NULL)\n+\t\treturn 0;\n+\tretvalue = fn(changes->first_refname, cb_data);\n+\tfor (i = 0; retvalue == 0 && i < changes->additional_refnames.nr; i++)\n+\t\tretvalue = fn(changes->additional_refnames.items[i].string, cb_data);\n+\treturn retvalue;\n+}\n+\n+struct change_head* get_change_head(struct change_table *heads,\n+\tconst char* refname)\n+{\n+\tstruct string_list_item *item = string_list_lookup(\n+\t\t&heads->refname_to_change_head, refname);\n+\n+\tif (!item)\n+\t\treturn NULL;\n+\n+\treturn (struct change_head *)item->util;\n+}\n+\ndiff --git a/change-table.h b/change-table.h\nnew file mode 100644\nindex 0000000000..023bca37d1\n--- /dev/null\n+++ b/change-table.h\n@@ -0,0 +1,127 @@\n+#ifndef CHANGE_TABLE_H\n+#define CHANGE_TABLE_H\n+\n+#include \"oidmap.h\"\n+\n+struct commit;\n+struct ref_filter;\n+\n+/*\n+ * This struct holds a list of change refs. The first element is stored inline,\n+ * to optimize for small lists.\n+ */\n+struct change_list {\n+\t/* Ref name for the first change in the list, or null if none.\n+\t *\n+\t * This field is private. Use for_each_change_in to read.\n+\t */\n+\tconst char* first_refname;\n+\t/* List of additional change refs. Note that this is empty if the list\n+\t * contains 0 or 1 elements.\n+\t *\n+\t * This field is private. Use for_each_change_in to read.\n+\t */\n+\tstruct string_list additional_refnames;\n+};\n+\n+/*\n+ * Holds information about the head of a single change.\n+ */\n+struct change_head {\n+\t/*\n+\t * The location pointed to by the head of the change. May be a commit or a\n+\t * metacommit.\n+\t */\n+\tstruct object_id head;\n+\t/*\n+\t * The content commit for the latest commit in the change. Always points to a\n+\t * real commit, never a metacommit.\n+\t */\n+\tstruct object_id content;\n+\t/*\n+\t * Abandoned: indicates that the content commit should be removed from the\n+\t * history.\n+\t *\n+\t * Hidden: indicates that the change is an inactive change from the\n+\t * hiddenmetas namespace. Such changes will be hidden from the user by\n+\t * default.\n+\t *\n+\t * Deleted: indicates that the change has been removed from the repository.\n+\t * That is the ref was deleted since the time this struct was created. Such\n+\t * entries should be ignored.\n+\t */\n+\tint abandoned:1,\n+\t\thidden:1,\n+\t\tremote:1,\n+\t\tdeleted:1;\n+};\n+\n+/*\n+ * Holds the list of change refs whose content points to a particular content\n+ * commit.\n+ */\n+struct commit_change_list_entry {\n+\tstruct oidmap_entry entry;\n+\tstruct change_list changes;\n+};\n+\n+/*\n+ * Holds information about the heads of each change, and permits effecient\n+ * lookup from a commit to the changes that reference it directly.\n+ *\n+ * All fields should be considered private. Use the change_table functions\n+ * to interact with this struct.\n+ */\n+struct change_table {\n+\t/**\n+\t * Memory pool for the objects allocated by the change table.\n+\t */\n+\tstruct mem_pool *memory_pool;\n+\t/* Map object_id to commit_change_list_entry structs. */\n+\tstruct oidmap oid_to_metadata_index;\n+\t/* List of ref names. The util value points to a change_head structure\n+\t * allocated from memory_pool.\n+\t */\n+\tstruct string_list refname_to_change_head;\n+};\n+\n+extern void change_table_init(struct change_table *to_initialize);\n+extern void change_table_clear(struct change_table *to_clear);\n+\n+/* Adds the given change head to the change_table struct */\n+extern void change_table_add(struct change_table *to_modify,\n+\tconst char *refname, struct commit *target);\n+\n+/* Adds the non-hidden local changes to the given change_table struct.\n+ */\n+extern void change_table_add_all_visible(struct change_table *to_modify,\n+\tstruct repository *repo);\n+\n+/*\n+ * Adds all changes matching the given ref filter to the given change_table\n+ * struct.\n+ */\n+extern void change_table_add_matching_filter(struct change_table *to_modify,\n+\tstruct repository* repo, struct ref_filter *filter);\n+\n+typedef int each_change_fn(const char *refname, void *cb_data);\n+\n+extern int change_table_has_change_referencing(struct change_table *changes,\n+\tconst struct object_id *referenced_commit_id);\n+\n+/* Iterates over all changes that reference the given commit. For metacommits,\n+ * this is the list of changes that point directly to that metacommit.\n+ * For normal commits, this is the list of changes that have this commit as\n+ * their latest content.\n+ */\n+extern int for_each_change_referencing(struct change_table *heads,\n+\tconst struct object_id *referenced_commit_id, each_change_fn fn, void *cb_data);\n+\n+/**\n+ * Returns the change head for the given refname. Returns NULL if no such change\n+ * exists.\n+ */\n+extern struct change_head* get_change_head(struct change_table *heads,\n+\tconst char* refname);\n+\n+#endif\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368292","messageId":"20190201030925.177124-6-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 6/8] evolve: add support for writing metacommits","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:23Z","receivedAt":"2019-02-01T03:09:49Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nmetacommit.c supports the creation of metacommits and\nadds the API needed to create and update changes.\n\nCreate the \"modify_change\" function that can be called from modification\ncommands like \"rebase\" and \"git amend\" to record obsolescences in the\nchange graph.\n\nCreate the \"record_metacommit\" function for recording more complicated\ncommit relationships in the commit graph.\n\nCreate the \"write_metacommit\" function for low-level creation of\nmetacommits.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n Makefile     |   1 +\n metacommit.c | 358 +++++++++++++++++++++++++++++++++++++++++++++++++++\n metacommit.h |  39 ++++++\n 3 files changed, 398 insertions(+)\n create mode 100644 metacommit.c\n create mode 100644 metacommit.h\n\ndiff --git a/Makefile b/Makefile\nindex 2e35e1421d..14a0decf01 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -929,6 +929,7 @@ LIB_OBJS += merge.o\n LIB_OBJS += merge-blobs.o\n LIB_OBJS += merge-recursive.o\n LIB_OBJS += mergesort.o\n+LIB_OBJS += metacommit.o\n LIB_OBJS += metacommit-parser.o\n LIB_OBJS += midx.o\n LIB_OBJS += name-hash.o\ndiff --git a/metacommit.c b/metacommit.c\nnew file mode 100644\nindex 0000000000..99bdda118f\n--- /dev/null\n+++ b/metacommit.c\n@@ -0,0 +1,358 @@\n+#include \"cache.h\"\n+#include \"metacommit.h\"\n+#include \"commit.h\"\n+#include \"change-table.h\"\n+#include \"refs.h\"\n+\n+void init_metacommit_data(struct metacommit_data *state)\n+{\n+\tmemset(state, 0, sizeof(*state));\n+}\n+\n+void clear_metacommit_data(struct metacommit_data *state)\n+{\n+\toid_array_clear(&state->replace);\n+\toid_array_clear(&state->origin);\n+}\n+\n+static void compute_default_change_name(struct commit *initial_commit,\n+\tstruct strbuf* result)\n+{\n+\tconst char *buffer = get_commit_buffer(initial_commit, NULL);\n+\tconst char *subject;\n+\tconst char *eol;\n+\tint len;\n+\tfind_commit_subject(buffer, &subject);\n+\teol = strchrnul(subject, '\\n');\n+\tfor (len = 0;subject < eol && len < 10; ++subject, ++len) {\n+\t\tchar next = *subject;\n+\t\tif (isspace(next))\n+\t\t\tcontinue;\n+\n+\t\tstrbuf_addch(result, next);\n+\t}\n+}\n+\n+/*\n+ * Computes a change name for a change rooted at the given initial commit. Good\n+ * change names should be memorable, unique, and easy to type. They are not\n+ * required to match the commit comment.\n+ */\n+static void compute_change_name(struct commit *initial_commit, struct strbuf* result)\n+{\n+\tstruct strbuf default_name;\n+\tstruct object_id unused;\n+\n+\tstrbuf_init(&default_name, 0);\n+\tif (initial_commit)\n+\t\tcompute_default_change_name(initial_commit, &default_name);\n+\telse\n+\t\tstrbuf_addstr(&default_name, \"change\");\n+\tstrbuf_addstr(result, \"refs/metas/\");\n+\tstrbuf_addbuf(result, &default_name);\n+\n+\t/* If there is already a change of this name, append a suffix */\n+\tif (!read_ref(result->buf, &unused)) {\n+\t\tint suffix = 2;\n+\t\tint original_length = result->len;\n+\n+\t\twhile (1) {\n+\t\t\tstrbuf_addf(result, \"%d\", suffix);\n+\t\t\tif (read_ref(result->buf, &unused))\n+\t\t\t\tbreak;\n+\t\t\tstrbuf_remove(result, original_length, result->len - original_length);\n+\t\t\t++suffix;\n+\t\t}\n+\t}\n+\n+\tstrbuf_release(&default_name);\n+}\n+\n+struct resolve_metacommit_callback_data\n+{\n+\tstruct change_table* active_changes;\n+\tstruct string_list *changes;\n+\tstruct oid_array *heads;\n+};\n+\n+static int resolve_metacommit_callback(const char *refname, void *cb_data)\n+{\n+\tstruct resolve_metacommit_callback_data *data = (struct resolve_metacommit_callback_data *)cb_data;\n+\tstruct change_head *chhead;\n+\n+\tchhead = get_change_head(data->active_changes, refname);\n+\n+\tif (data->changes)\n+\t\tstring_list_append(data->changes, refname)->util = &(chhead->head);\n+\tif (data->heads)\n+\t\toid_array_append(data->heads, &(chhead->head));\n+\n+\treturn 0;\n+}\n+\n+/*\n+ * Produces the final form of a metacommit based on the current change refs.\n+ */\n+static void resolve_metacommit(\n+\tstruct repository* repo,\n+\tstruct change_table* active_changes,\n+\tconst struct metacommit_data *to_resolve,\n+\tstruct metacommit_data *resolved_output,\n+\tstruct string_list *to_advance,\n+\tint allow_append)\n+{\n+\tint i;\n+\tint len = to_resolve->replace.nr;\n+\tstruct resolve_metacommit_callback_data cbdata;\n+\tint old_change_list_length = to_advance->nr;\n+\tstruct commit* content;\n+\n+\toidcpy(&resolved_output->content, &to_resolve->content);\n+\n+\t/* First look for changes that point to any of the replacement edges in the\n+\t * metacommit. These will be the changes that get advanced by this\n+\t * metacommit. */\n+\tresolved_output->abandoned = to_resolve->abandoned;\n+\tcbdata.active_changes = active_changes;\n+\tcbdata.changes = to_advance;\n+\tcbdata.heads = &(resolved_output->replace);\n+\n+\tif (allow_append) {\n+\t\tfor (i = 0; i < len; i++) {\n+\t\t\tint old_number = resolved_output->replace.nr;\n+\t\t\tfor_each_change_referencing(active_changes, &(to_resolve->replace.oid[i]),\n+\t\t\t\tresolve_metacommit_callback, &cbdata);\n+\t\t\t/* If no changes were found, use the unresolved value. */\n+\t\t\tif (old_number == resolved_output->replace.nr)\n+\t\t\t\toid_array_append(&(resolved_output->replace), &(to_resolve->replace.oid[i]));\n+\t\t}\n+\t}\n+\n+\tcbdata.changes = NULL;\n+\tcbdata.heads = &(resolved_output->origin);\n+\n+\tlen = to_resolve->origin.nr;\n+\tfor (i = 0; i < len; i++) {\n+\t\tint old_number = resolved_output->origin.nr;\n+\t\tfor_each_change_referencing(active_changes, &(to_resolve->origin.oid[i]),\n+\t\t\tresolve_metacommit_callback, &cbdata);\n+\t\tif (old_number == resolved_output->origin.nr)\n+\t\t\toid_array_append(&(resolved_output->origin), &(to_resolve->origin.oid[i]));\n+\t}\n+\n+\t/* If no changes were advanced by this metacommit, we'll need to create a new\n+\t * one. */\n+\tif (to_advance->nr == old_change_list_length) {\n+\t\tstruct strbuf change_name;\n+\n+\t\tstrbuf_init(&change_name, 80);\n+\t\tcontent = lookup_commit_reference_gently(repo, &(to_resolve->content), 1);\n+\n+\t\tcompute_change_name(content, &change_name);\n+\t\tstring_list_append(to_advance, change_name.buf);\n+\t\tstrbuf_release(&change_name);\n+\t}\n+}\n+\n+static void lookup_commits(\n+\tstruct repository *repo,\n+\tstruct oid_array *to_lookup,\n+\tstruct commit_list **result)\n+{\n+\tint i = to_lookup->nr;\n+\n+\twhile (--i >= 0) {\n+\t\tstruct object_id *next = &(to_lookup->oid[i]);\n+\t\tstruct commit *commit = lookup_commit_reference_gently(repo, next, 1);\n+\t\tcommit_list_insert(commit, result);\n+\t}\n+}\n+\n+#define PARENT_TYPE_PREFIX \"parent-type \"\n+\n+/*\n+ * Creates a new metacommit object with the given content. Writes the object\n+ * id of the newly-created commit to result.\n+ */\n+int write_metacommit(struct repository *repo, struct metacommit_data *state,\n+\tstruct object_id *result)\n+{\n+\tstruct commit_list *parents = NULL;\n+\tstruct strbuf comment;\n+\tint i;\n+\tstruct commit *content;\n+\n+\tstrbuf_init(&comment, strlen(PARENT_TYPE_PREFIX)\n+\t\t+ 1 + 2 * (state->origin.nr + state->replace.nr));\n+\tlookup_commits(repo, &state->origin, &parents);\n+\tlookup_commits(repo, &state->replace, &parents);\n+\tcontent = lookup_commit_reference_gently(repo, &state->content, 1);\n+\tif (!content) {\n+\t\tstrbuf_release(&comment);\n+\t\tfree_commit_list(parents);\n+\t\treturn -1;\n+\t}\n+\tcommit_list_insert(content, &parents);\n+\n+\tstrbuf_addstr(&comment, PARENT_TYPE_PREFIX);\n+\tstrbuf_addstr(&comment, state->abandoned ? \"a\" : \"c\");\n+\tfor (i = 0; i < state->replace.nr; i++)\n+\t\tstrbuf_addstr(&comment, \" r\");\n+\n+\tfor (i = 0; i < state->origin.nr; i++)\n+\t\tstrbuf_addstr(&comment, \" o\");\n+\n+\t/* The parents list will be freed by this call. */\n+\tcommit_tree(comment.buf, comment.len, repo->hash_algo->empty_tree, parents,\n+\t\tresult, NULL, NULL);\n+\n+\tstrbuf_release(&comment);\n+\treturn 0;\n+}\n+\n+/*\n+ * Returns true iff the given metacommit is abandoned, has one or more origin\n+ * parents, or has one or more replacement parents.\n+ */\n+static int is_nontrivial_metacommit(struct metacommit_data *state)\n+{\n+\treturn state->replace.nr || state->origin.nr || state->abandoned;\n+}\n+\n+/*\n+ * Records the relationships described by the given metacommit in the\n+ * repository.\n+ *\n+ * If override_change is NULL (the default), an attempt will be made\n+ * to append to existing changes wherever possible instead of creating new ones.\n+ * If override_change is non-null, only the given change ref will be updated.\n+ *\n+ * options is a bitwise combination of the UPDATE_OPTION_* flags.\n+ */\n+int record_metacommit(struct repository *repo,\n+\tconst struct metacommit_data *metacommit,\n+\tconst char* override_change, int options, struct strbuf *err)\n+{\n+\tstatic const char *msg = \"updating change\";\n+\tstruct metacommit_data resolved_metacommit;\n+\tstruct string_list changes;\n+\tstruct object_id commit_target;\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct object_id old_head_working;\n+\tconst struct object_id *old_head;\n+\tstruct change_table chtable;\n+\tint i;\n+\tint ret = 0;\n+\tint force = (options & UPDATE_OPTION_FORCE);\n+\n+\tinit_metacommit_data(&resolved_metacommit);\n+\tstring_list_init(&changes, 1);\n+\n+\tchange_table_init(&chtable);\n+\n+\tchange_table_add_all_visible(&chtable, repo);\n+\n+\tresolve_metacommit(repo, &chtable, metacommit, &resolved_metacommit, &changes,\n+\t\t(options & UPDATE_OPTION_NOAPPEND) == 0);\n+\n+\tif (override_change) {\n+\t\told_head = &old_head_working;\n+\t\tstring_list_clear(&changes, 0);\n+\t\tif (get_oid_committish(override_change, &old_head_working))\n+\t\t\t/* ...then this is a newly-created change */\n+\t\t\told_head = &null_oid;\n+\t\telse if (!force) {\n+\t\t\tif (!oid_array_readonly_contains(&(resolved_metacommit.replace),\n+\t\t\t\t&old_head_working)) {\n+\t\t\t\t/* Attempted non-fast-forward change */\n+\t\t\t\tstrbuf_addf(err, _(\"non-fast-forward update to '%s'\"),\n+\t\t\t\t\toverride_change);\n+\t\t\t\tret = -1;\n+\t\t\t\tgoto cleanup;\n+\t\t\t}\n+\t\t}\n+\t\t/* The expected \"current\" head of the change is stored in the util\n+\t\t * pointer. */\n+\t\tstring_list_append(&changes, override_change)->util = (void*)old_head;\n+\t}\n+\n+\tif (is_nontrivial_metacommit(&resolved_metacommit)) {\n+\t\t/* If there are any origin or replacement parents, create a new metacommit\n+\t\t * object. */\n+\t\tif (write_metacommit(repo, &resolved_metacommit, &commit_target) < 0) {\n+\t\t\tret = -1;\n+\t\t\tgoto cleanup;\n+\t\t}\n+\t} else\n+\t\t/* If the metacommit would only contain a content commit, point to the\n+\t\t * commit itself rather than creating a trivial metacommit. */\n+\t\toidcpy(&commit_target, &(resolved_metacommit.content));\n+\n+\t/* If a change already exists with this target and we're not forcing an\n+\t * update to some specific override_change && change, there's nothing to do.\n+\t */\n+\tif (!override_change \n+\t\t&& change_table_has_change_referencing(&chtable, &commit_target))\n+\t\t/* Not an error */\n+\t\tgoto cleanup;\n+\n+\ttransaction = ref_transaction_begin(err);\n+\n+\t/* Update the refs for each affected change */\n+\tif (!transaction)\n+\t\tret = -1;\n+\telse {\n+\t\tfor (i = 0; i < changes.nr; i++) {\n+\t\t\tstruct string_list_item *it = &(changes.items[i]);\n+\n+\t\t\t/* The expected current head of the change is stored in the util pointer.\n+\t\t\t * It is null if the change should be newly-created. */\n+\t\t\tif (it->util) {\n+\t\t\t\tif (ref_transaction_update(transaction, it->string, &commit_target,\n+\t\t\t\t\tforce ? NULL : it->util, 0, msg, err))\n+\n+\t\t\t\t\tret = -1;\n+\t\t\t} else {\n+\t\t\t\tif (ref_transaction_create(transaction, it->string,\n+\t\t\t\t\t&commit_target, 0, msg, err))\n+\n+\t\t\t\t\tret = -1;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (!ret)\n+\t\t\tif (ref_transaction_commit(transaction, err))\n+\t\t\t\tret = -1;\n+\t}\n+\n+cleanup:\n+\tref_transaction_free(transaction);\n+\tstring_list_clear(&changes, 0);\n+\tclear_metacommit_data(&resolved_metacommit);\n+\tchange_table_clear(&chtable);\n+\treturn ret;\n+}\n+\n+/*\n+ * Should be invoked after a command that has \"modify\" semantics - commands that\n+ * create a new commit based on an old commit and treat the new one as a\n+ * replacement for the old one. This method records the replacement in the\n+ * change graph, such that a future evolve operation will rebase children of\n+ * the old commit onto the new commit.\n+ */\n+void modify_change(\n+\tstruct repository *repo,\n+\tconst struct object_id *old_commit,\n+\tconst struct object_id *new_commit,\n+\tstruct strbuf *err)\n+{\n+\tstruct metacommit_data metacommit;\n+\n+\tinit_metacommit_data(&metacommit);\n+\toidcpy(&(metacommit.content), new_commit);\n+\toid_array_append(&(metacommit.replace), old_commit);\n+\n+\trecord_metacommit(repo, &metacommit, NULL, 0, err);\n+\n+\tclear_metacommit_data(&metacommit);\n+}\ndiff --git a/metacommit.h b/metacommit.h\nnew file mode 100644\nindex 0000000000..3a0912938c\n--- /dev/null\n+++ b/metacommit.h\n@@ -0,0 +1,39 @@\n+#ifndef METACOMMIT_H\n+#define METACOMMIT_H\n+\n+/* If specified, non-fast-forward changes are permitted. */\n+#define UPDATE_OPTION_FORCE     0x0001\n+/* If specified, no attempt will be made to append to existing changes.\n+ * Normally, if a metacommit points to a commit in its replace or origin\n+ * list and an existing change points to that same commit as its content, the\n+ * new metacommit will attempt to append to that same change. This may replace\n+ * the commit parent with one or more metacommits from the head of the appended\n+ * changes. This option disables this behavior, and will always create a new\n+ * change rather than reusing existing changes. */\n+#define UPDATE_OPTION_NOAPPEND  0x0002\n+\n+/* Metacommit Data */\n+\n+struct metacommit_data {\n+\tstruct object_id content;\n+\tstruct oid_array replace;\n+\tstruct oid_array origin;\n+\tint abandoned;\n+};\n+\n+extern void init_metacommit_data(struct metacommit_data *state);\n+\n+extern void clear_metacommit_data(struct metacommit_data *state);\n+\n+extern int record_metacommit(struct repository *repo,\n+\tconst struct metacommit_data *metacommit,\n+\tconst char* override_change, int options, struct strbuf *err);\n+\n+extern void modify_change(struct repository *repo,\n+\tconst struct object_id *old_commit, const struct object_id *new_commit,\n+\tstruct strbuf *err);\n+\n+extern int write_metacommit(struct repository *repo, struct metacommit_data *state,\n+\tstruct object_id *result);\n+\n+#endif\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368293","messageId":"20190201030925.177124-7-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 7/8] evolve: implement the git change command","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:24Z","receivedAt":"2019-02-01T03:09:51Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nImplement the git change update command, which\nare sufficient for constructing change graphs.\n\nFor example, to create a new change (a stable name) that refers to HEAD:\n\ngit change update -c HEAD\n\nTo record a rebase or amend in the change graph:\n\ngit change update -c <new_commit> -r <old_commit>\n\nTo record a cherry-pick in the change graph:\n\ngit change update -c <new_commit> -o <original_commit>\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n .gitignore       |   1 +\n Makefile         |   1 +\n builtin.h        |   1 +\n builtin/change.c | 175 +++++++++++++++++++++++++++++++++++++++++++++++\n git.c            |   1 +\n 5 files changed, 179 insertions(+)\n create mode 100644 builtin/change.c\n\ndiff --git a/.gitignore b/.gitignore\nindex 0d77ea5894..8a084ac38b 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -26,6 +26,7 @@\n /git-branch\n /git-bundle\n /git-cat-file\n+/git-change\n /git-check-attr\n /git-check-ignore\n /git-check-mailmap\ndiff --git a/Makefile b/Makefile\nindex 14a0decf01..e40edd6763 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1044,6 +1044,7 @@ BUILTIN_OBJS += builtin/blame.o\n BUILTIN_OBJS += builtin/branch.o\n BUILTIN_OBJS += builtin/bundle.o\n BUILTIN_OBJS += builtin/cat-file.o\n+BUILTIN_OBJS += builtin/change.o\n BUILTIN_OBJS += builtin/check-attr.o\n BUILTIN_OBJS += builtin/check-ignore.o\n BUILTIN_OBJS += builtin/check-mailmap.o\ndiff --git a/builtin.h b/builtin.h\nindex 6538932e99..d2d39d9da8 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -137,6 +137,7 @@ extern int cmd_blame(int argc, const char **argv, const char *prefix);\n extern int cmd_branch(int argc, const char **argv, const char *prefix);\n extern int cmd_bundle(int argc, const char **argv, const char *prefix);\n extern int cmd_cat_file(int argc, const char **argv, const char *prefix);\n+extern int cmd_change(int argc, const char **argv, const char *prefix);\n extern int cmd_checkout(int argc, const char **argv, const char *prefix);\n extern int cmd_checkout_index(int argc, const char **argv, const char *prefix);\n extern int cmd_check_attr(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/change.c b/builtin/change.c\nnew file mode 100644\nindex 0000000000..06cbb50679\n--- /dev/null\n+++ b/builtin/change.c\n@@ -0,0 +1,175 @@\n+#include \"builtin.h\"\n+#include \"ref-filter.h\"\n+#include \"parse-options.h\"\n+#include \"metacommit.h\"\n+#include \"config.h\"\n+\n+static const char * const builtin_change_usage[] = {\n+\tN_(\"git change update [--force] [--replace <treeish>...] [--origin <treesih>...] [--content <newtreeish>]\"),\n+\tNULL\n+};\n+\n+static const char * const builtin_update_usage[] = {\n+\tN_(\"git change update [--force] [--replace <treeish>...] [--origin <treesih>...] [--content <newtreeish>]\"),\n+\tNULL\n+};\n+\n+struct update_state {\n+\tint options;\n+\tconst char* change;\n+\tconst char* content;\n+\tstruct string_list replace;\n+\tstruct string_list origin;\n+};\n+\n+static void init_update_state(struct update_state *state)\n+{\n+\tmemset(state, 0, sizeof(*state));\n+\tstate->content = \"HEAD\";\n+\tstring_list_init(&state->replace, 0);\n+\tstring_list_init(&state->origin, 0);\n+}\n+\n+static void clear_update_state(struct update_state *state)\n+{\n+\tstring_list_clear(&state->replace, 0);\n+\tstring_list_clear(&state->origin, 0);\n+}\n+\n+static int update_option_parse_replace(const struct option *opt,\n+\t\t\t\t\t\t\tconst char *arg, int unset)\n+{\n+\tstruct update_state *state = opt->value;\n+\tstring_list_append(&state->replace, arg);\n+\treturn 0;\n+}\n+\n+static int update_option_parse_origin(const struct option *opt,\n+\t\t\t\t\t\t\tconst char *arg, int unset)\n+{\n+\tstruct update_state *state = opt->value;\n+\tstring_list_append(&state->origin, arg);\n+\treturn 0;\n+}\n+\n+static int resolve_commit(const char *committish, struct object_id *result)\n+{\n+\tstruct commit *commit;\n+\tif (get_oid_committish(committish, result))\n+\t\tdie(_(\"Failed to resolve '%s' as a valid revision.\"), committish);\n+\tcommit = lookup_commit_reference(the_repository, result);\n+\tif (!commit)\n+\t\tdie(_(\"Could not parse object '%s'.\"), committish);\n+\toidcpy(result, &commit->object.oid);\n+\treturn 0;\n+}\n+\n+static void resolve_commit_list(const struct string_list *commitsish_list,\n+\tstruct oid_array* result)\n+{\n+\tint i;\n+\tfor (i = 0; i < commitsish_list->nr; i++) {\n+\t\tstruct string_list_item *item = &commitsish_list->items[i];\n+\t\tstruct object_id next;\n+\t\tresolve_commit(item->string, &next);\n+\t\toid_array_append(result, &next);\n+\t}\n+}\n+\n+/*\n+ * Given the command-line options for the update command, fills in a\n+ * metacommit_data with the corresponding changes.\n+ */\n+static void get_metacommit_from_command_line(\n+\tconst struct update_state* commands, struct metacommit_data *result)\n+{\n+\tresolve_commit(commands->content, &(result->content));\n+\tresolve_commit_list(&(commands->replace), &(result->replace));\n+\tresolve_commit_list(&(commands->origin), &(result->origin));\n+}\n+\n+static int perform_update(\n+\tstruct repository *repo,\n+\tconst struct update_state *state,\n+\tstruct strbuf *err)\n+{\n+\tstruct metacommit_data metacommit;\n+\tint ret;\n+\n+\tinit_metacommit_data(&metacommit);\n+\n+\tget_metacommit_from_command_line(state, &metacommit);\n+\n+\tret = record_metacommit(repo, &metacommit, state->change, state->options, err);\n+\n+\tclear_metacommit_data(&metacommit);\n+\n+\treturn ret;\n+}\n+\n+static int change_update(int argc, const char **argv, const char* prefix)\n+{\n+\tint result;\n+\tint force = 0;\n+\tint newchange = 0;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct update_state state;\n+\tstruct option options[] = {\n+\t\t{ OPTION_CALLBACK, 'r', \"replace\", &state, N_(\"commit\"),\n+\t\t\tN_(\"marks the given commit as being obsolete\"),\n+\t\t\t0, update_option_parse_replace },\n+\t\t{ OPTION_CALLBACK, 'o', \"origin\", &state, N_(\"commit\"),\n+\t\t\tN_(\"marks the given commit as being the origin of this commit\"),\n+\t\t\t0, update_option_parse_origin },\n+\t\tOPT_BOOL('F', \"force\", &force,\n+\t\t\tN_(\"overwrite an existing change of the same name\")),\n+\t\tOPT_STRING('c', \"content\", &state.content, N_(\"commit\"),\n+\t\t\t\t N_(\"identifies the new content commit for the change\")),\n+\t\tOPT_STRING('g', \"change\", &state.change, N_(\"commit\"),\n+\t\t\t\t N_(\"name of the change to update\")),\n+\t\tOPT_BOOL('n', \"new\", &newchange,\n+\t\t\tN_(\"create a new change - do not append to any existing change\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tinit_update_state(&state);\n+\n+\targc = parse_options(argc, argv, prefix, options, builtin_update_usage, 0);\n+\n+\tif (force) state.options |= UPDATE_OPTION_FORCE;\n+\tif (newchange) state.options |= UPDATE_OPTION_NOAPPEND;\n+\n+\tresult = perform_update(the_repository, &state, &err);\n+\n+\tif (result < 0) {\n+\t\terror(\"%s\", err.buf);\n+\t\tstrbuf_release(&err);\n+\t}\n+\n+\tclear_update_state(&state);\n+\n+\treturn result;\n+}\n+\n+int cmd_change(int argc, const char **argv, const char *prefix)\n+{\n+\t/* No options permitted before subcommand currently */\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tint result = 1;\n+\n+\targc = parse_options(argc, argv, prefix, options, builtin_change_usage,\n+\t\tPARSE_OPT_STOP_AT_NON_OPTION);\n+\n+\tif (argc < 1)\n+\t\tusage_with_options(builtin_change_usage, options);\n+\telse if (!strcmp(argv[0], \"update\"))\n+\t\tresult = change_update(argc, argv, prefix);\n+\telse {\n+\t\terror(_(\"Unknown subcommand: %s\"), argv[0]);\n+\t\tusage_with_options(builtin_change_usage, options);\n+\t}\n+\n+\treturn result ? 1 : 0;\n+}\ndiff --git a/git.c b/git.c\nindex 0ce0e13f0f..f59f887238 100644\n--- a/git.c\n+++ b/git.c\n@@ -453,6 +453,7 @@ static struct cmd_struct commands[] = {\n \t{ \"branch\", cmd_branch, RUN_SETUP | DELAY_PAGER_CONFIG },\n \t{ \"bundle\", cmd_bundle, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"cat-file\", cmd_cat_file, RUN_SETUP },\n+\t{ \"change\", cmd_change, RUN_SETUP },\n \t{ \"check-attr\", cmd_check_attr, RUN_SETUP },\n \t{ \"check-ignore\", cmd_check_ignore, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"check-mailmap\", cmd_check_mailmap, RUN_SETUP },\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368294","messageId":"20190201030925.177124-8-sxenos@google.com","threadId":"50372","inReplyTo":"20190201030925.177124-1-sxenos@google.com","subject":"[PATCH v4 8/8] evolve: add the git change list command","fromName":"","fromEmail":"sxenos@google.com","sentAt":"2019-02-01T03:09:25Z","receivedAt":"2019-02-01T03:09:54Z","isPatch":true,"sender":{"key":"sxenos@google.com","avatar":null},"body":"From: Stefan Xenos <sxenos@google.com>\n\nThis command lists the ongoing changes from the refs/metas\nnamespace.\n\nSigned-off-by: Stefan Xenos <sxenos@google.com>\n---\n builtin/change.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 51 insertions(+)\n\ndiff --git a/builtin/change.c b/builtin/change.c\nindex 06cbb50679..33a5983bc7 100644\n--- a/builtin/change.c\n+++ b/builtin/change.c\n@@ -5,15 +5,64 @@\n #include \"config.h\"\n \n static const char * const builtin_change_usage[] = {\n+\tN_(\"git change list [<pattern>...]\"),\n \tN_(\"git change update [--force] [--replace <treeish>...] [--origin <treesih>...] [--content <newtreeish>]\"),\n \tNULL\n };\n \n+static const char * const builtin_list_usage[] = {\n+\tN_(\"git change list [<pattern>...]\"),\n+\tNULL\n+};\n+\n static const char * const builtin_update_usage[] = {\n \tN_(\"git change update [--force] [--replace <treeish>...] [--origin <treesih>...] [--content <newtreeish>]\"),\n \tNULL\n };\n \n+static int change_list(int argc, const char **argv, const char* prefix)\n+{\n+\tstruct option options[] = {\n+\t\tOPT_END()\n+\t};\n+\tstruct ref_filter filter;\n+\t/* TODO: Sorting temporarily disabled. See comments, below.\n+\t * struct ref_sorting *sorting = ref_default_sorting(); */\n+\tstruct ref_format format = REF_FORMAT_INIT;\n+\tstruct ref_array array;\n+\tint i;\n+\n+\targc = parse_options(argc, argv, prefix, options, builtin_list_usage, 0);\n+\n+\tsetup_ref_filter_porcelain_msg();\n+\n+\tmemset(&filter, 0, sizeof(filter));\n+\tmemset(&array, 0, sizeof(array));\n+\n+\tfilter.kind = FILTER_REFS_CHANGES;\n+\tfilter.name_patterns = argv;\n+\n+\tfilter_refs(&array, &filter, FILTER_REFS_CHANGES);\n+\n+\t/* TODO: This causes a crash. It sets one of the atom_value handlers to\n+\t * something invalid, which causes a crash later when we call\n+\t * show_ref_array_item. Figure out why this happens and put back the sorting.\n+\t *ref_array_sort(sorting, &array); */\n+\n+\tif (!format.format)\n+\t\tformat.format = \"%(refname:lstrip=1)\";\n+\n+\tif (verify_ref_format(&format))\n+\t\tdie(_(\"unable to parse format string\"));\n+\n+\tfor (i = 0; i < array.nr; i++)\n+\t\tshow_ref_array_item(array.items[i], &format);\n+\n+\tref_array_clear(&array);\n+\n+\treturn 0;\n+}\n+\n struct update_state {\n \tint options;\n \tconst char* change;\n@@ -164,6 +213,8 @@ int cmd_change(int argc, const char **argv, const char *prefix)\n \n \tif (argc < 1)\n \t\tusage_with_options(builtin_change_usage, options);\n+\telse if (!strcmp(argv[0], \"list\"))\n+\t\tresult = change_list(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"update\"))\n \t\tresult = change_update(argc, argv, prefix);\n \telse {\n-- \n2.20.1.611.gfbb209baf1-goog\n\n"},{"id":"368333","messageId":"f9c093bb-0225-1e0a-94ed-3854fa1f9ee2@ramsayjones.plus.com","threadId":"50372","inReplyTo":"20190201030925.177124-5-sxenos@google.com","subject":"Re: [PATCH v4 5/8] evolve: add the change-table structure","fromName":"Ramsay Jones","fromEmail":"ramsay@ramsayjones.plus.com","sentAt":"2019-02-01T15:46:02Z","receivedAt":"2019-02-01T15:46:10Z","isPatch":true,"sender":{"key":"ramsay@ramsayjones.plus.com","avatar":"https://avatars.githubusercontent.com/u/33702710?v=4"},"body":"\n\nOn 01/02/2019 03:09, sxenos@google.com wrote:\n> From: Stefan Xenos <sxenos@google.com>\n> \n> A change table stores a list of changes, and supports efficient lookup\n> from a commit hash to the list of changes that reference that commit\n> directly.\n> \n> It can be used to look up content commits or metacommits at the head\n> of a change, but does not support lookup of commits referenced as part\n> of the commit history.\n> \n> Signed-off-by: Stefan Xenos <sxenos@google.com>\n> ---\n>  Makefile       |   1 +\n>  change-table.c | 176 +++++++++++++++++++++++++++++++++++++++++++++++++\n>  change-table.h | 127 +++++++++++++++++++++++++++++++++++\n>  3 files changed, 304 insertions(+)\n>  create mode 100644 change-table.c\n>  create mode 100644 change-table.h\n> \n\n[snip]\n\n> diff --git a/change-table.h b/change-table.h\n> new file mode 100644\n> index 0000000000..023bca37d1\n> --- /dev/null\n> +++ b/change-table.h\n> @@ -0,0 +1,127 @@\n> +#ifndef CHANGE_TABLE_H\n> +#define CHANGE_TABLE_H\n> +\n> +#include \"oidmap.h\"\n> +\n> +struct commit;\n> +struct ref_filter;\n> +\n> +/*\n> + * This struct holds a list of change refs. The first element is stored inline,\n> + * to optimize for small lists.\n> + */\n> +struct change_list {\n> +\t/* Ref name for the first change in the list, or null if none.\n> +\t *\n> +\t * This field is private. Use for_each_change_in to read.\n> +\t */\n> +\tconst char* first_refname;\n> +\t/* List of additional change refs. Note that this is empty if the list\n> +\t * contains 0 or 1 elements.\n> +\t *\n> +\t * This field is private. Use for_each_change_in to read.\n> +\t */\n> +\tstruct string_list additional_refnames;\n> +};\n> +\n> +/*\n> + * Holds information about the head of a single change.\n> + */\n> +struct change_head {\n> +\t/*\n> +\t * The location pointed to by the head of the change. May be a commit or a\n> +\t * metacommit.\n> +\t */\n> +\tstruct object_id head;\n> +\t/*\n> +\t * The content commit for the latest commit in the change. Always points to a\n> +\t * real commit, never a metacommit.\n> +\t */\n> +\tstruct object_id content;\n> +\t/*\n> +\t * Abandoned: indicates that the content commit should be removed from the\n> +\t * history.\n> +\t *\n> +\t * Hidden: indicates that the change is an inactive change from the\n> +\t * hiddenmetas namespace. Such changes will be hidden from the user by\n> +\t * default.\n> +\t *\n> +\t * Deleted: indicates that the change has been removed from the repository.\n> +\t * That is the ref was deleted since the time this struct was created. Such\n> +\t * entries should be ignored.\n> +\t */\n> +\tint abandoned:1,\n> +\t\thidden:1,\n> +\t\tremote:1,\n> +\t\tdeleted:1;\n\nThis causes sparse to issue errors about 'dubious one-bit signed\nbitfield' for each of these fields (and for each file which #includes\nthis header file).\n\nThe field type should be 'unsigned int', thus:\n\n-- >8 --\ndiff --git a/change-table.h b/change-table.h\nindex 85bb19c3bf..1c385e076e 100644\n--- a/change-table.h\n+++ b/change-table.h\n@@ -50,10 +50,10 @@ struct change_head {\n         * That is the ref was deleted since the time this struct was created. Such\n         * entries should be ignored.\n         */\n-       int abandoned:1,\n-               hidden:1,\n-               remote:1,\n-               deleted:1;\n+       unsigned int abandoned:1,\n+                       hidden:1,\n+                       remote:1,\n+                       deleted:1;\n };\n \n /*\n-- >8 --\n\n[Note: this diff was against the v3 series].\n\nATB,\nRamsay Jones\n\n"}]}