针对 Perforce 用户的 Git

我已经使用 Perforce 很多年了。我希望在我的个人代码中使用 git,但是我看到的所有 git 教程要么假设你是一个完整的源代码控制 n00b (这使得它们非常乏味) ,要么假设你已经习惯了 svn (我不是)。

我了解 p4,也理解分布式源代码控制系统背后的理念(所以我不需要推销,谢谢)。我想要的是一个从 p4命令到等效 git 命令的转换表,以及没有等效 p4命令的“ can’t live without”命令。

因为我怀疑每个 p4用户都使用不同的 p4子集,下面是我在 p4中经常做的一些事情,我希望能够在 git 中完成,这些事情在我看过的文档中并不明显:

  1. 在一个客户端中创建多个挂起的变更列表
  2. 编辑挂起的变更列表(也是 p4 change)
  3. 查看所有挂起的变更列表(p4 changes -s pending)的列表
  4. 我的客户端(p4 opened)或挂起的变更列表(p4 describe)中所有更改过的文件的列表
  5. 查看挂起的变更列表的差异(我为此使用了一个包装器脚本,它使用 p4 diffp4 describe)
  6. 对于给定的文件,查看哪些提交的变更列表影响哪些行(p4 annotate)
  7. 对于给定的文件,请参见影响该文件的变更列表的描述列表(p4 log)
  8. 提交待定的变更列表(p4 submit -c)
  9. 中止挂起的变更列表(p4 revert)

其中很多都围绕着“变更列表”。“变更列表”是 p4术语。 git 的等价术语是什么?

这听起来像是 Git 用户使用分支来代替 p4调用的变更列表。有点令人困惑,因为 p4也有一个叫做分支的东西,尽管它们看起来只是模糊的相关概念。(尽管我一直认为 p4的分支概念非常奇怪,但它与经典的 RCS 分支概念又有所不同。)

无论如何... 我不确定如何完成我通常在 p4变更列表中使用 git 的分支所做的工作。在 p4中,我可以这样做:

$ p4 edit a.txt
$ p4 change a.txt
Change 12345 created.

此时,我有一个包含 a.txt 的变更列表。我可以编辑描述并继续工作而不提交变更列表。另外,如果我需要对其他一些文件进行一些修改,比如在代码的其他层中修复一个 bug,我可以在同一个客户端中进行:

$ p4 edit z.txt
$ p4 change z.txt
Change 12346 created.

现在,在同一个客户端中有两个不同的变更列表。我可以同时处理这些问题,而且不需要做任何事情来“切换”它们。当需要提交的时候,我可以单独提交:

$ p4 submit -c 12346  # this will submit the changes to z.txt
$ p4 submit -c 12345  # this will submit the changes to a.txt

我不知道如何在 Git 中复制这个。从我的实验来看,似乎 git add并没有与当前的分支相关联。据我所知,当我 git commit时,无论我当时在哪个分支,它都会提交我 git add化的所有文件:

$ git init
Initialized empty Git repository in /home/laurence/git-playground/.git/
$ ls
a.txt  w.txt  z.txt
$ git add -A .
$ git commit
Initial commit.
3 files changed, 3 insertions(+), 0 deletions(-)
create mode 100644 a.txt
create mode 100644 w.txt
create mode 100644 z.txt
$ vi a.txt z.txt
2 files to edit
$ git status
# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#   modified:   a.txt
#   modified:   z.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
$ git branch aardvark
$ git checkout aardvark
M   a.txt
M   z.txt
Switched to branch 'aardvark'
$ git add a.txt
$ git checkout master
M   a.txt
M   z.txt
Switched to branch 'master'
$ git branch zebra
$ git checkout zebra
M   a.txt
M   z.txt
Switched to branch 'zebra'
$ git add z.txt
$ git status
# On branch zebra
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   a.txt
#   modified:   z.txt
#
$ git checkout aardvark
M   a.txt
M   z.txt
Switched to branch 'aardvark'
$ git status
# On branch aardvark
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   modified:   a.txt
#   modified:   z.txt

在这个示例中,aardvark 和 zebra 分支似乎包含完全相同的一组更改,并且根据 git status的输出,似乎在任何一个分支中执行提交都会产生相同的效果。我做错什么了吗?

24191 次浏览

I don't have enough p4 experience to produce an actual cheat sheet, but there are at least some similarities to fall back on. A p4 "changeset" is a git "commit".

Changes to your local work space get added to the "index" with git add, and the index later gets committed with git commit. So the index is your pending changelist, for all intents and purposes.

You look at changes with git diff and git status, where git diff usually shows changes between the workspace and the index, but git diff --cached shows changes between the index and the repository (= your pending changelist).

For more in depth information, I recommend http://progit.org/book/. Since you know version control in general, you can probably skim a lot of it and extract the git-specific information...

I haven't used perforce much so this may not be exactly be a 1:1 translation. Then again distributed source control systems like git and mercurial have a different workflow anyway so there really isn't (and there shouldn't) be a 1:1 translation. Anyway, here goes:

  • Create multiple pending changelists -> Use branches instead. In git branches are light and quick, takes less than a second to create and typically less than two seconds to merge. Don't be afraid of branching and rebase often.

    git branch new-branch-name
    git checkout new-branch-name
    

    Or do it all in one line:

    git checkout -b new-branch-name
    
  • See a list of all pending changelists -> Since the equivalent of multiple pending changelist is multiple branches just view the branches:

    git branch
    

    If you want to view remote branches as well:

    git branch -a
    

    It is considered good practice to immediately delete a branch after a successful merge so you don't have to keep track of which branch are pending to be merged and which have already been merged.

  • List all changed files -> For a single pending "changelist" in a specific branch git has a concept of the index or cache. In order to commit a change you must first add files to this index. This allows you to manually select which group of files represent a single change or to ignore irrelevant files. To see the status of which files are added, or not to this index just do:

    git status
    
  • See a diff of a pending changelist -> There are two parts to this. First to see a diff between the working directory and the index:

    git diff
    

    But if you want to know the diff between what you're typing now and the last commit then you are really asking for a diff between the working directory+index and the HEAD:

    git diff HEAD
    
  • For a given file, see which submitted changelists affected which lines -> This is easy:

    git blame filename
    

    or even better, if you are in a windowing environment:

    git gui blame filename
    

    Git gui takes longer to parse the file (it was written in tcl instead of C) but it has lots of neat features including the ability to "time travel" back into the past by clicking on a commit ID. I only wish they'd implement a feature to "time travel" to the future so I can find out how a given bug will finally be resolved ;-)

  • For a given file, see a list of the descriptions of the changelists that affected the file -> also easy:

    git log filename
    

    But git log is a much more powerful tool than just this. In fact most of my personal scripts piggyback off-of git log to read the repository. Read the man page.

  • Submit a pending changelist -> Also easy:

    git commit
    

See my answer to a previous question to see my typical git workflow: Learning Git. Need to know if I am on the right track

If you follow the workflow I outlined then you'll find tools like gitk to be much more valuable since it allows you to clearly see groups of changes.


Additional answer:

Git is very flexible and there are several ways to do what you describe. The thing to remember is to always start a new branch for each feature you're working on. This means the master branch isn't touched so you can always go back to it to do bug fixes. Working in git one should almost always start with:

git checkout -b new-feature-a

Now you can edit file a.txt. To work concurrently on another feature do:

git checkout master
git checkout -b new-feature-z

Now you can edit file z.txt. To switch back to a.txt:

git checkout new-feature-a

But wait, there are changes to new-feature-z and git won't let you switch branches. At this point you have two choices. The first is the simplest, commit all changes to the current branch:

git add .
git commit
git checkout new-feature-a

This is what I'd recommend. But if you are really not ready to commit the code, you can temporarily stash it:

git stash

Now you can switch to branch new-feature-a. To go back to the code you were working on just pop the stash:

git checkout new-feature-z
git stash pop

When all is done merge back all changes to master:

git merge --no-ff new-feature-a
git merge --no-ff new-feature-z

Because merges are so quick and easy (easy because conflicts are so rare and conflict resolution, when one does happen, not too hard) we use branches in git for everything.

Here's another example of a common use of branches in git that you don't see in other source control tools (except perhaps mercurial):

Need to keep changing your config files to reflect your dev environment? Then use a branch:

git checkout -b dev-config

Now edit your config files in your favourite editor then commit changes:

git add .
git commit

Now every new branch can start from the dev-config branch instead of master:

git checkout dev-config
git checkout -b new-feature-branch

Once you're done remove the edits in dev-config from new-feature-branch using interactive rebase:

git rebase -i master

Delete the commits you don't want then save. Now you have a clean branch without custom config edits. Time to merge back to master:

git checkout master
git merge --no-ff new-feature-branch
# because master have changed, it's a good idea to rebase dev-config:
git checkout dev-config
git rebase master

It should be noted that removing edits with git rebase -i even works when all changes happen in the same file. Git remembers changes, not file content*.

*note: actually, technically not entirely true but as a user that's what it feels like


More additional answer:

So, from you comments it looks like you want to have two branches to exist simultaneously so you can test how the combined code works. Well, this is a good way to illustrate the power and flexibility of branches.

First, a word on the implication of cheap branching and modifiable history on your workflow. When I was using CVS and SVN I was always a bit reluctant to commit. That's because committing unstable code would inevitably f**k up other people's working code. But with git I lost that fear. That's because in git other people won't get my changes until I merge them to master. So now I find myself committing code every 5 lines I write. You don't need perfect foresight to commit. You just need to change your mindset: commit-to-branch==add-to-changeset, merge-to-master==commit-changeset.

So, back to examples. Here's how I would do it. Say you have a branch new-feature-z and you want to test it with new-feature-a. I would just create a new branch to test it:

# assume we are currently in branch new-feature-z
# branch off this branch for testing
git checkout -b feature-z-and-feature-a
# now temporarily merge new-feature-a
git merge --no-ff new-feature-a

Now you can test. If you need to modify something to make feature-z work with feature-a then do so. If so you can merge back the changes to the relevant branch. Use git rebase -i to remove irrelevant changes from the merge.

Alternatively, you can also use git rebase to temporarily change the base of new-feature-z to point to new-feature-a:

# assume we are currently in branch new-feature-z
git rebase new-feature-a

Now the branch history is modified so that new-feature-z will be based off new-feature-a instead of master. Now you can test. Any changes committed in this branch will belong to the branch new-feature-z. If you need to modify new-feature-a just switch back to it and the rebase to get the new changes:

git checkout new-feature-a
# edit code, add, commit etc..
git checkout new-feature-z
git rebase new-feature-a
# now new-feature-z will contain new changes from new-feature-a

When you're done, simply rebase back to master to remove changes from new-feature-a:

# assume we are currently in branch new-feature-z
git rebase master

Don't be afraid to start a new branch. Don't be afraid to start a throwaway branch. Don't be afraid to throw away branches. And since merge==submit and commit==add-to-changeset don't be afraid to commit often. Remember, commit is a developer's ultimate undo tool.

Oh, and another thing, in git deleted branches still exist in your repository. So if you've accidentally deleted something that you later realise is useful after all you can always get it back by searching the history. So don't be afraid to throw away branches.

This doesn't answer your question specifically, but I don't know if you are aware that a 2 User, 5 Workspace version of perforce is free to download and use from the perforce website.

This way you can use perforce at home for your personal projects if you wish. The one annoyance is the 5 workspaces which can be a bit limiting, but its pretty incredible to have perforce available for home use.

I suffer like you with the lack of the "changelist" concept which is not exactly the same as git branches.

I would write a small script that will create a changelist file with the list of files in that changelist.

Another command to submit just a certain changelist by simply calling git commit -a @change_list_contents.txt and then "git commit"

Hope that helps, Elias

There is a more lightweight alternative in git that could form part of your workflow; using the git staging area.

I often just make changes then submit as several commits (e.g. add debug statements, refactor, actually fix a bug). Rather than setting up your perforce changelists, then make changes, then submit, you can just make your changes then choose how to submit them (optionally using the git staging area).

You can commit particular files from the command line with:

git commit a.txt
git commit z.txt

Or explicitly staging the files first:

git add a.txt
git commit
git add z.txt
git commit

git gui will let you select lines or hunks from within files to build up a commit in the staging area. This is very useful if you have changes in one file that you want to be in different commits. Having moved from git to perforce and this is one thing that I really miss.

There is a small caveat to bear in mind with this workflow. If you make changes A and B to a file, test the file, then commit A then you haven't tested that commit (independently of B).

Having used both Perforce and git fairly extensively, there's only one way I can see to get anywhere near Perforce changelists with git.

The first thing to understand is that to correctly implement this functionality in git in such a way that it's a not a complete kluge, e.g. trying to shoehorn it into branches, would require the following change: git would require multiple staging areas for a single branch.

Perforce changelists permit a workflow that simply has no equivalent in git. Consider the following workflow:

Check out a branch
Modify file A and add it to changelist 1
Modify file B and add it to changelist 2

If you try to do this using branches in git you'll wind up with two branches, one of which has the changes to file A, the other has the changes to file B, but no place where you can see the changes to both files A and B at the same time.

The closest approximation I can see is to use git add . -p and then use the 'a' and 'd' sub-commands to select or reject entire files. However that's not quite the same, and the difference here stems from a fundamental disparity in the general modus operandi of the two systems.

Git (and subversion, not that it matters for this discussion) allow a file to be changed without telling anyone about this ahead of time. You just change a file, and then let git sort it all out when you commit the changes. Perforce requires you to actively check out a file before changes are allowed, and it is for this reason that changelists have to exist. In essence, Perforce requires you to add a file to the index before changing it. Hence the necessity for multiple changelists in Perforce, and also the reason why git has no equivalent. It simply doesn't need them.

With Git 2.27 (Q2 2020), "git p4" learned four new hooks and also "--no-verify" option to bypass them (and the existing "p4-pre-submit" hook).

See commit 1ec4a0a, commit 38ecf75, commit cd1e0dc (14 Feb 2020), and commit 4935c45, commit aa8b766, commit 9f59ca4, commit 6b602a2 (11 Feb 2020) by Ben Keene (seraphire).
(Merged by Junio C Hamano -- gitster -- in commit 5f2ec21, 22 Apr 2020)

git-p4: add p4 submit hooks

Signed-off-by: Ben Keene

The git command "commit" supports a number of hooks that support changing the behavior of the commit command.

The git-p4.py program only has one existing hook, "p4-pre-submit".

This command occurs early in the process.

There are no hooks in the process flow for modifying the P4 changelist text programmatically.

Adds 3 new hooks to git-p4.py to the submit option.

The new hooks are:

  • p4-prepare-changelist - Execute this hook after the changelist file has been created.
    The hook will be executed even if the --prepare-p4-only option is set.
    This hook ignores the --no-verify option in keeping with the existing behavior of git commit.

  • p4-changelist - Execute this hook after the user has edited the changelist.
    Do not execute this hook if the user has selected the --prepare-p4-only option.
    This hook will honor the --no-verify, following the conventions of git commit.

  • p4-post-changelist - Execute this hook after the P4 submission process has completed successfully.
    This hook takes no parameters and is executed regardless of the --no-verify option.

It's return value will not be checked.

The calls to the new hooks: p4-prepare-changelist, p4-changelist, and p4-post-changelist should all be called inside the try-finally block.


Before Git 2.28 (Q3 2020), the "--prepare-p4-only" option is supposed to stop after replaying one changeset, but kept going (by mistake?)

See commit 2dfdd70 (12 May 2020) by Ben Keene (seraphire).
(Merged by Junio C Hamano -- gitster -- in commit 7a8fec9, 02 Jun 2020)

git-p4.py: fix --prepare-p4-only error with multiple commits

Signed-off-by: Ben Keene

When using git p4 submit with the --prepare-p4-only option, the program should prepare a single p4 changelist and notify the user that more commits are pending and then stop processing.

A bug has been introduced by the p4-changelist hook feature that causes the program to continue to try and process all pending changelists at the same time.

The function applyCommit returns True when applying the commit was successful and the program should continue.
However, when the optional flag --prepare-p4-only is set, the program should stop after the first application.

Change the logic in the run method for P4Submit to check for the flag --prepare-p4-only after successfully completing the applyCommit method.

If more than 1 commit is pending submission to P4, the method will properly prepare the P4 changelist, however it will still exit the application with an exitcode of 1.

The current documentation does not define what the exit code should be in this condition.