-->

Git commits for each push

2020-07-31 06:46发布

问题:

Is there a way to retrieve a list of commits for the latest push? For example let's assume i'm doing 10 local commits but a single push, is there a git command showing just these 10 commits?

回答1:

You say

i'm doing 10 local commits

so let's assume you did one of

git clone whatever
git pull

immediately before you did

# edit stuff
git commit -a
# repeat 9 more times

and then

git push

Now you can see these 10 commits with the command

git log origin/master@{1}..origin/master

But if you did

git fetch

anytime between your commits, you have spoiled your reflog of where origin/master was when you began your local work. You will have to remember how often origin/master was changed by git fetch and adjust {1} to {2} (it counts how many updates---not revisions---you want to go back).



回答2:

The short answer is that you can't do what you want reliably: Git itself does not record git push actions. But there are some things you can do. Specifically, in the Git doing the git push or in the Git receiving the git push, at the time of the push itself, you can get this information. How to save it, deal with it, and use it later is up to you.

(I'd also argue that this is not a good idea: don't try to group things by push, group them some other way. For instance, in a CI system, group them by request, with requests being updated dynamically. If build request #30 had commits A, B, and C as "new since request was created" five seconds go due to a previous push, but now has A, B, and D instead, do a CI check of A-B-D, not one of A-B-C, then one of remove-C-add-D. Read through the rest of this answer to understand what's going on here.)

The pre-push hook

The Git that is sending commits will run a pre-push hook, if it's present. The pre-push hook on the sending Git gets four informational items per, um, let's call it "per thingy" for the moment:

  • local ref
  • local OID/SHA-1/hash
  • remote ref
  • remote OID/SHA-1/hash

Let's say you did:

git push origin refs/heads/master:refs/tags/v1.1

Here the local ref is thus refs/heads/master. The hash ID—which is a SHA-1 hash today, but internals of Git now call "OID" (meaning object ID) for future-proofing when Git switches to SHA-256, but you can just call it "hash" to avoid TLA syndrome1—is whatever commit hash ID your refs/heads/master identifies. The remote ref will be refs/tags/v1.1, and the remote hash will probably be all-zeros, since this is presumably a new lightweight tag you'd like to create.

If you ran instead:

git push origin master develop

your hook would get two thingies. One would mention refs/heads/master twice and the other would mention refs/heads/develop twice: the local and remote master branch, and the local and remote develop branch, that you're pushing in one big git push operation. The hash IDs would be those for your local master and for their master, and for your local develop and for their develop.

Using these hash IDs, you can see which commit(s) are new to them. If their hash ID is in your Git repository, you can also see whether you're asking them to remove any commits—or more precisely, make them unreachable. For much more about reachability, see Think Like (a) Git.

Some of these hash IDs may be all-zeros. Such a hash ID means "there is no such name". For git push, the remote hash will be all-zeros if you're asking their Git to delete the reference. The local hash will be all-zeros if you don't have the reference (which is meaningful only if you're asking them to delete too).


1TLA stands for Three Letter Acronym. Compare with ETLA, which is an Extended TLA with more than three letters.


The pre-receive, update, and post-receive hooks

The Git that is receiving commits, and being asked to update its references, will run the pre-receive hook and post-receive hooks if they exist. These will get as many "thingies" as there are update requests. It will also run the update hook, if it exists, once per thingy.

The pre-receive hook gets three informational items per thingy:

  • the current (old) OID/hash
  • the proposed new OID/hash
  • the reference

The current hash tells you what the name currently represents. For instance, with our tag-creation example, the current hash would be all-zeros. The proposed new hash is the object ID that the pushing Git is asking you, the receiving Git, to use as the new hash ID for the updated reference. The reference is of course the reference to be updated.

With our two-branches-to-update example, the two hashes for refs/heads/master would be the current master commit and the proposed new master commit. These are both likely to be valid hashes, rather than all-zeros, but at most one can be all-zeros. The old-hash is all-zero if you, the receiving Git, don't have the reference yet (i.e., the branch master is all new to you); the new-hash is all-zero if you, the receiving Git, are being asked to delete the reference.

A pre-push hook's job is to read through all the proposed updates and verify whether this is OK. If so, the pre-push hook should exit 0 ("true" in shell-exit-status-speak). If not, the pre-push hook can print output intended to inform the user running git push why the push is being rejected—that user will see this output with the word remote: stuck in front of it—and then exit nonzero, to reject the entire push.

At the time the pre-receive hook runs, the receiving Git has access to all the objects proposed. That is, if the guy doing the push ran git push origin master develop and this meant sending three new master commits and one new develop commit, the pre-receive hook on the server runs after the server has collected all four new commits, and any other objects required by those commits. The new objects are "in quarantine", in a holding area somewhere. If the push is rejected, the quarantine area is thrown away without incorporating the commits into the main repository.2 The entire push is aborted at this stage.

If the pre-receive hook allows the push—or does not exist—the push goes on to its next stage, where the receiving Git actually does update each reference, one at a time. At this time the receiving Git runs the update hook for each reference, giving it (as arguments, rather than as stdin) the reference, the old hash, and the new hash (note the different order). The update hook can inspect items as before, and then either accept or reject this particular update. Whether or not the update is rejected, the receiving continues with the next reference. So the update hook has only a local view—one reference at a time—but finer-grained accept/reject control.

Finally, after all the updates have been done or rejected, if any references were updated, the receiving Git runs the post-receive hook, if it exists. This gets the same kind of stdin lines as the pre-receive hook. The hook should exit zero, because the push is already done. The locks on the various reference updates have been released so the hook should not look up the reference names in the Git repository—they might have changed already due to another push!


2This "quarantine area" was new in Git 2.13; before that, the new objects went in even if they ended up being unused, only to have to be thrown out later. On really big servers (e.g., GitHub) this caused a lot of pain.


Enumerating commits

Given an old hash ID and a new hash ID, the command:

git rev-list $old..$new

enumerates all the commits that are reachable from $new but not from $old. For a git push, these are the new commits just added, for instance.

Its counterpart:

git rev-list $new..$old

enumerates the commits reachable from $old that are no longer reachable from $new. These are the commits removed by a push, for instance.

Note that it's possible to do both at the same time! An update might remove one commit and replace it with a new-and-improved variant.

You can get both sets of commits in one shot using:

git rev-list $old...$new

To make this output useful, you must add --left-right to insert markers about which commits are reachable only from $old and which ones are reachable only from $new.

You can get counts of reachable commits using git rev-list --count. Adding --left-right to the three-dot variant gives you two counts: this is how git status computes the ahead-and-behind counts, for instance. (Well, git status has the code compiled in, so it's easier than it would be in a script—but this lets you do what git status does, in a script.)

Conclusion

A push enumeration is possible, but only by using information Git keeps only during the push event. Once the push is done, or rejected, you have only the resulting graph. Other than recording something about the push itself—e.g., sending mail informing someone that a push event added 3 commits and removed 1—this isn't generally very useful, which is why Git doesn't keep this itself.

If there's something important about some particular commit grouping, you can record this in the graph itself. For instance, suppose you have a feature that requires three steps to achieve:

  • upgrade existing routines that weren't capable, so that they are more capable
  • add new routines to do a new thing
  • add the top-level integration that uses the old and new routines in the new way

In this case, instead of going from:

...--o--*   <-- master

to:

...--o--*--A--B--C   <-- master

where A through C are the new commits that do these three steps, consider pushing the new graph as:

...--o--*---------M   <-- master
         \       /
          A--B--C

Here M is a new merge commit. Set its merge message to (a better variant of) integrate new feature. Set the commit messages for A, B, and C to augment existing routines, add new routines, and integrate old and new routines to support new feature. This merge bubble—the A-B-C chain—isolates the feature, so that if something is really terrible, you can revert the entire merge by reverting M, and if something is slightly broken, you can test commits A through C individually to figure out what. You can do either or both of these—revert entire merge, or not; test commits individually, or not—because all the information is saved forever, in the graph.



回答3:

Thanks for the support everyone especially @torek for his smart and interesting answer, this is how I did it with gitlab API and python:

import json
import requests

def checkAsset(obj):
    status=0
    #status=0 modified, status=1 new file, status=2 deleted
    if (obj['new_path']==obj['old_path'] and obj['new_file']==False):
        status=0
    elif (obj['new_path']==obj['old_path'] and obj['new_file']==True):
        status=1
    elif (obj['new_path']==obj['old_path'] and obj['deleted_file']==True):
        status=2
    else:
        status=0
    return status


headers = {'Private-Token': 'XXXXXXXXXXXXXX'}
#this API gives you all commits grouped by pushes
pushes= "https://gitlab.XXXXX/api/v4/projects/{{projectID}}/events??target_type=issue&action=pushed"

r = requests.get(pushes, headers=headers)

latestPushes=json.loads(r.content)

lastPush=latestPushes[0]
i=0
while lastPush['push_data']['ref']!= 'master':
    i+=1
    lastPush=latestPushes[i]

commitNumber=lastPush['push_data']['commit_count']
if (commitNumber > 30):
    raise Exception("Could not compare, too many commits in one push")
initCommit=lastPush['push_data']['commit_from']
latestCommit=lastPush['push_data']['commit_to']

compareApi= "https://gitlab.XXXXXXXXXXX/api/v4/projects/{{projectID}}/repository/compare?from="+str(initCommit)+"&to="+str(latestCommit)

r = requests.get(compareApi, headers=headers)

compareJson=json.loads(r.content)

diffs=compareJson['diffs']

Mlist=[]
Alist=[]
Dlist=[]
for asset in diffs:
        status=checkAsset(asset)
        if status==0:
            Mlist.append(asset['new_path'].encode('ascii','ignore'))
        elif status==1:
            Alist.append(asset['new_path'].encode('ascii','ignore'))
        else:
            Dlist.append(asset['new_path'].encode('ascii','ignore'))


回答4:

You can check your commit history by using below command.

git log


标签: git git-bash