From 3ea2cfd488ed70e03d8d6f10e601784926db8008 Mon Sep 17 00:00:00 2001 From: Luke Diamand Date: Thu, 21 Apr 2011 20:50:23 +0100 Subject: [PATCH 1/3] git-p4: add option to preserve user names Patches from git passed into p4 end up with the committer being identified as the person who ran git-p4. With "submit --preserve-user", git-p4 modifies the p4 changelist (after it has been submitted), setting the p4 author field. The submitter is required to have sufficient p4 permissions or git-p4 refuses to proceed. If the git author is not known to p4, the submit will be abandoned unless git-p4.allowMissingP4Users is true. Signed-off-by: Luke Diamand Acked-by: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 179 ++++++++++++++++++++++++++------- contrib/fast-import/git-p4.txt | 29 ++++++ t/t9800-git-p4.sh | 84 ++++++++++++++++ 3 files changed, 254 insertions(+), 38 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 78e5b3aaf..36e3d871f 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -474,6 +474,47 @@ class Command: self.usage = "usage: %prog [options]" self.needsGit = True +class P4UserMap: + def __init__(self): + self.userMapFromPerforceServer = False + + def getUserCacheFilename(self): + home = os.environ.get("HOME", os.environ.get("USERPROFILE")) + return home + "/.gitp4-usercache.txt" + + def getUserMapFromPerforceServer(self): + if self.userMapFromPerforceServer: + return + self.users = {} + self.emails = {} + + for output in p4CmdList("users"): + if not output.has_key("User"): + continue + self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" + self.emails[output["Email"]] = output["User"] + + + s = '' + for (key, val) in self.users.items(): + s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1)) + + open(self.getUserCacheFilename(), "wb").write(s) + self.userMapFromPerforceServer = True + + def loadUserMapFromCache(self): + self.users = {} + self.userMapFromPerforceServer = False + try: + cache = open(self.getUserCacheFilename(), "rb") + lines = cache.readlines() + cache.close() + for line in lines: + entry = line.strip().split("\t") + self.users[entry[0]] = entry[1] + except IOError: + self.getUserMapFromPerforceServer() + class P4Debug(Command): def __init__(self): Command.__init__(self) @@ -554,13 +595,16 @@ class P4RollBack(Command): return True -class P4Submit(Command): +class P4Submit(Command, P4UserMap): def __init__(self): Command.__init__(self) + P4UserMap.__init__(self) self.options = [ optparse.make_option("--verbose", dest="verbose", action="store_true"), optparse.make_option("--origin", dest="origin"), optparse.make_option("-M", dest="detectRenames", action="store_true"), + # preserve the user, requires relevant p4 permissions + optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"), ] self.description = "Submit changes from git to the perforce depot." self.usage += " [name of git branch to submit into perforce depot]" @@ -568,6 +612,7 @@ class P4Submit(Command): self.origin = "" self.detectRenames = False self.verbose = False + self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true" self.isWindows = (platform.system() == "Windows") def check(self): @@ -602,6 +647,75 @@ class P4Submit(Command): return result + def p4UserForCommit(self,id): + # Return the tuple (perforce user,git email) for a given git commit id + self.getUserMapFromPerforceServer() + gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id) + gitEmail = gitEmail.strip() + if not self.emails.has_key(gitEmail): + return (None,gitEmail) + else: + return (self.emails[gitEmail],gitEmail) + + def checkValidP4Users(self,commits): + # check if any git authors cannot be mapped to p4 users + for id in commits: + (user,email) = self.p4UserForCommit(id) + if not user: + msg = "Cannot find p4 user for email %s in commit %s." % (email, id) + if gitConfig('git-p4.allowMissingP4Users').lower() == "true": + print "%s" % msg + else: + die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg) + + def lastP4Changelist(self): + # Get back the last changelist number submitted in this client spec. This + # then gets used to patch up the username in the change. If the same + # client spec is being used by multiple processes then this might go + # wrong. + results = p4CmdList("client -o") # find the current client + client = None + for r in results: + if r.has_key('Client'): + client = r['Client'] + break + if not client: + die("could not get client spec") + results = p4CmdList("changes -c %s -m 1" % client) + for r in results: + if r.has_key('change'): + return r['change'] + die("Could not get changelist number for last submit - cannot patch up user details") + + def modifyChangelistUser(self, changelist, newUser): + # fixup the user field of a changelist after it has been submitted. + changes = p4CmdList("change -o %s" % changelist) + for c in changes: + if c.has_key('User'): + c['User'] = newUser + input = marshal.dumps(changes[0]) + result = p4CmdList("change -f -i", stdin=input) + for r in result: + if r.has_key('code'): + if r['code'] == 'error': + die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data'])) + if r.has_key('data'): + print("Updated user field for changelist %s to %s" % (changelist, newUser)) + return + die("Could not modify user field of changelist %s to %s" % (changelist, newUser)) + + def canChangeChangelists(self): + # check to see if we have p4 admin or super-user permissions, either of + # which are required to modify changelists. + results = p4CmdList("-G protects %s" % self.depotPath) + for r in results: + if r.has_key('perm'): + if r['perm'] == 'admin': + return 1 + if r['perm'] == 'super': + return 1 + return 0 + def prepareSubmitTemplate(self): # remove lines in the Files section that show changes to files outside the depot path we're committing into template = "" @@ -631,6 +745,9 @@ class P4Submit(Command): def applyCommit(self, id): print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id)) + if self.preserveUser: + (p4User, gitEmail) = self.p4UserForCommit(id) + if not self.detectRenames: # If not explicitly set check the config variable self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true" @@ -781,8 +898,13 @@ class P4Submit(Command): editor = read_pipe("git var GIT_EDITOR").strip() system(editor + " " + fileName) + if gitConfig("git-p4.skipSubmitEditCheck") == "true": + checkModTime = False + else: + checkModTime = True + response = "y" - if os.stat(fileName).st_mtime <= mtime: + if checkModTime and (os.stat(fileName).st_mtime <= mtime): response = "x" while response != "y" and response != "n": response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ") @@ -795,6 +917,14 @@ class P4Submit(Command): if self.isWindows: submitTemplate = submitTemplate.replace("\r\n", "\n") p4_write_pipe("submit -i", submitTemplate) + + if self.preserveUser: + if p4User: + # Get last changelist number. Cannot easily get it from + # the submit command output as the output is unmarshalled. + changelist = self.lastP4Changelist() + self.modifyChangelistUser(changelist, p4User) + else: for f in editedFiles: p4_system("revert \"%s\"" % f); @@ -831,6 +961,10 @@ class P4Submit(Command): if len(self.origin) == 0: self.origin = upstream + if self.preserveUser: + if not self.canChangeChangelists(): + die("Cannot preserve user names without p4 super-user or admin permissions") + if self.verbose: print "Origin branch is " + self.origin @@ -858,6 +992,9 @@ class P4Submit(Command): commits.append(line.strip()) commits.reverse() + if self.preserveUser: + self.checkValidP4Users(commits) + while len(commits) > 0: commit = commits[0] commits = commits[1:] @@ -877,11 +1014,12 @@ class P4Submit(Command): return True -class P4Sync(Command): +class P4Sync(Command, P4UserMap): delete_actions = ( "delete", "move/delete", "purge" ) def __init__(self): Command.__init__(self) + P4UserMap.__init__(self) self.options = [ optparse.make_option("--branch", dest="branch"), optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"), @@ -1236,41 +1374,6 @@ class P4Sync(Command): print ("Tag %s does not match with change %s: file count is different." % (labelDetails["label"], change)) - def getUserCacheFilename(self): - home = os.environ.get("HOME", os.environ.get("USERPROFILE")) - return home + "/.gitp4-usercache.txt" - - def getUserMapFromPerforceServer(self): - if self.userMapFromPerforceServer: - return - self.users = {} - - for output in p4CmdList("users"): - if not output.has_key("User"): - continue - self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">" - - - s = '' - for (key, val) in self.users.items(): - s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1)) - - open(self.getUserCacheFilename(), "wb").write(s) - self.userMapFromPerforceServer = True - - def loadUserMapFromCache(self): - self.users = {} - self.userMapFromPerforceServer = False - try: - cache = open(self.getUserCacheFilename(), "rb") - lines = cache.readlines() - cache.close() - for line in lines: - entry = line.strip().split("\t") - self.users[entry[0]] = entry[1] - except IOError: - self.getUserMapFromPerforceServer() - def getLabels(self): self.labels = {} diff --git a/contrib/fast-import/git-p4.txt b/contrib/fast-import/git-p4.txt index e09da445b..b6986f0eb 100644 --- a/contrib/fast-import/git-p4.txt +++ b/contrib/fast-import/git-p4.txt @@ -110,6 +110,12 @@ is not your current git branch you can also pass that as an argument: You can override the reference branch with the --origin=mysourcebranch option. +The Perforce changelists will be created with the user who ran git-p4. If you +use --preserve-user then git-p4 will attempt to create Perforce changelists +with the Perforce user corresponding to the git commit author. You need to +have sufficient permissions within Perforce, and the git users need to have +Perforce accounts. Permissions can be granted using 'p4 protect'. + If a submit fails you may have to "p4 resolve" and submit manually. You can continue importing the remaining changes with @@ -196,6 +202,29 @@ able to find the relevant client. This client spec will be used to both filter the files cloned by git and set the directory layout as specified in the client (this implies --keep-path style semantics). +git-p4.skipSubmitModTimeCheck + + git config [--global] git-p4.skipSubmitModTimeCheck false + +If true, submit will not check if the p4 change template has been modified. + +git-p4.preserveUser + + git config [--global] git-p4.preserveUser false + +If true, attempt to preserve user names by modifying the p4 changelists. See +the "--preserve-user" submit option. + +git-p4.allowMissingPerforceUsers + + git config [--global] git-p4.allowMissingP4Users false + +If git-p4 is setting the perforce user for a commit (--preserve-user) then +if there is no perforce user corresponding to the git author, git-p4 will +stop. With allowMissingPerforceUsers set to true, git-p4 will use the +current user (i.e. the behavior without --preserve-user) and carry on with +the perforce commit. + Implementation Details... ========================= diff --git a/t/t9800-git-p4.sh b/t/t9800-git-p4.sh index a52347395..4fb0e24f8 100755 --- a/t/t9800-git-p4.sh +++ b/t/t9800-git-p4.sh @@ -12,6 +12,8 @@ test_description='git-p4 tests' GITP4=$GIT_BUILD_DIR/contrib/fast-import/git-p4 P4DPORT=10669 +export P4PORT=localhost:$P4DPORT + db="$TRASH_DIRECTORY/db" cli="$TRASH_DIRECTORY/cli" git="$TRASH_DIRECTORY/git" @@ -129,6 +131,88 @@ test_expect_success 'clone bare' ' rm -rf "$git" && mkdir "$git" ' +p4_add_user() { + name=$1 + fullname=$2 + p4 user -f -i < /dev/null ; then + return 0 + else + echo "file $file not modified by user $user" 1>&2 + return 1 + fi +} + +# Test username support, submitting as user 'alice' +test_expect_success 'preserve users' ' + p4_add_user alice Alice && + p4_add_user bob Bob && + p4_grant_admin alice && + "$GITP4" clone --dest="$git" //depot && + cd "$git" && + echo "username: a change by alice" >> file1 && + echo "username: a change by bob" >> file2 && + git commit --author "Alice " -m "a change by alice" file1 && + git commit --author "Bob " -m "a change by bob" file2 && + git config git-p4.skipSubmitEditCheck true && + P4EDITOR=touch P4USER=alice P4PASSWD=secret "$GITP4" commit --preserve-user && + p4_check_commit_author file1 alice && + p4_check_commit_author file2 bob && + cd "$TRASH_DIRECTORY" && + rm -rf "$git" && mkdir "$git" +' + +# Test username support, submitting as bob, who lacks admin rights. Should +# not submit change to p4 (git diff should show deltas). +test_expect_success 'refuse to preserve users without perms' ' + "$GITP4" clone --dest="$git" //depot && + cd "$git" && + echo "username-noperms: a change by alice" >> file1 && + git commit --author "Alice " -m "perms: a change by alice" file1 && + ! P4EDITOR=touch P4USER=bob P4PASSWD=secret "$GITP4" commit --preserve-user && + ! git diff --exit-code HEAD..p4/master > /dev/null && + cd "$TRASH_DIRECTORY" && + rm -rf "$git" && mkdir "$git" +' + +# What happens with unknown author? Without allowMissingP4Users it should fail. +test_expect_success 'preserve user where author is unknown to p4' ' + "$GITP4" clone --dest="$git" //depot && + cd "$git" && + git config git-p4.skipSubmitEditCheck true + echo "username-bob: a change by bob" >> file1 && + git commit --author "Bob " -m "preserve: a change by bob" file1 && + echo "username-unknown: a change by charlie" >> file1 && + git commit --author "Charlie " -m "preserve: a change by charlie" file1 && + ! P4EDITOR=touch P4USER=alice P4PASSWD=secret "$GITP4" commit --preserve-user && + ! git diff --exit-code HEAD..p4/master > /dev/null && + echo "$0: repeat with allowMissingP4Users enabled" && + git config git-p4.allowMissingP4Users true && + git config git-p4.preserveUser true && + P4EDITOR=touch P4USER=alice P4PASSWD=secret "$GITP4" commit && + git diff --exit-code HEAD..p4/master > /dev/null && + p4_check_commit_author file1 alice && + cd "$TRASH_DIRECTORY" && + rm -rf "$git" && mkdir "$git" +' + test_expect_success 'shutdown' ' pid=`pgrep -f p4d` && test -n "$pid" && From ecdba36da6143bc00ade66b655bcff910f3e93b3 Mon Sep 17 00:00:00 2001 From: Luke Diamand Date: Sat, 7 May 2011 11:19:43 +0100 Subject: [PATCH 2/3] git-p4: small improvements to user-preservation . Slightly more paranoid checking of results from 'p4 change' . Remove superfluous "-G" . Don't modify the username if it is unchanged. . Add a comment in the change template to show what is going to be done. Signed-off-by: Luke Diamand Acked-By: Pete Wyckoff Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index 36e3d871f..e66a7df90 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -690,10 +690,15 @@ class P4Submit(Command, P4UserMap): def modifyChangelistUser(self, changelist, newUser): # fixup the user field of a changelist after it has been submitted. changes = p4CmdList("change -o %s" % changelist) - for c in changes: - if c.has_key('User'): - c['User'] = newUser - input = marshal.dumps(changes[0]) + if len(changes) != 1: + die("Bad output from p4 change modifying %s to user %s" % + (changelist, newUser)) + + c = changes[0] + if c['User'] == newUser: return # nothing to do + c['User'] = newUser + input = marshal.dumps(c) + result = p4CmdList("change -f -i", stdin=input) for r in result: if r.has_key('code'): @@ -707,7 +712,7 @@ class P4Submit(Command, P4UserMap): def canChangeChangelists(self): # check to see if we have p4 admin or super-user permissions, either of # which are required to modify changelists. - results = p4CmdList("-G protects %s" % self.depotPath) + results = p4CmdList("protects %s" % self.depotPath) for r in results: if r.has_key('perm'): if r['perm'] == 'admin': @@ -865,6 +870,10 @@ class P4Submit(Command, P4UserMap): if self.interactive: submitTemplate = self.prepareLogMessage(template, logMessage) + + if self.preserveUser: + submitTemplate = submitTemplate + ("\n######## Actual user %s, modified after commit\n" % p4User) + if os.environ.has_key("P4DIFF"): del(os.environ["P4DIFF"]) diff = "" From 848de9c3831f7e476ea58e3ee426aa63fb8f6870 Mon Sep 17 00:00:00 2001 From: Luke Diamand Date: Fri, 13 May 2011 20:46:00 +0100 Subject: [PATCH 3/3] git-p4: warn if git authorship won't be retained If the git commits you are submitting contain changes made by other people, the authorship will not be retained. Change git-p4 to warn of this and to note that --preserve-user can be used to solve the problem (if you have suitable permissions). The warning can be disabled. Add a test case and update documentation. Signed-off-by: Luke Diamand Signed-off-by: Junio C Hamano --- contrib/fast-import/git-p4 | 33 +++++++++++++++++++++++++-- contrib/fast-import/git-p4.txt | 7 ++++++ t/t9800-git-p4.sh | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/contrib/fast-import/git-p4 b/contrib/fast-import/git-p4 index e66a7df90..98d2aee67 100755 --- a/contrib/fast-import/git-p4 +++ b/contrib/fast-import/git-p4 @@ -614,6 +614,7 @@ class P4Submit(Command, P4UserMap): self.verbose = False self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true" self.isWindows = (platform.system() == "Windows") + self.myP4UserId = None def check(self): if len(p4CmdList("opened ...")) > 0: @@ -721,6 +722,25 @@ class P4Submit(Command, P4UserMap): return 1 return 0 + def p4UserId(self): + if self.myP4UserId: + return self.myP4UserId + + results = p4CmdList("user -o") + for r in results: + if r.has_key('User'): + self.myP4UserId = r['User'] + return r['User'] + die("Could not find your p4 user id") + + def p4UserIsMe(self, p4User): + # return True if the given p4 user is actually me + me = self.p4UserId() + if not p4User or p4User != me: + return False + else: + return True + def prepareSubmitTemplate(self): # remove lines in the Files section that show changes to files outside the depot path we're committing into template = "" @@ -750,8 +770,7 @@ class P4Submit(Command, P4UserMap): def applyCommit(self, id): print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id)) - if self.preserveUser: - (p4User, gitEmail) = self.p4UserForCommit(id) + (p4User, gitEmail) = self.p4UserForCommit(id) if not self.detectRenames: # If not explicitly set check the config variable @@ -890,6 +909,11 @@ class P4Submit(Command, P4UserMap): newdiff += "+" + line f.close() + if self.checkAuthorship and not self.p4UserIsMe(p4User): + submitTemplate += "######## git author %s does not match your p4 account.\n" % gitEmail + submitTemplate += "######## Use git-p4 option --preserve-user to modify authorship\n" + submitTemplate += "######## Use git-p4 config git-p4.skipUserNameCheck hides this message.\n" + separatorLine = "######## everything below this line is just the diff #######\n" [handle, fileName] = tempfile.mkstemp() @@ -1001,6 +1025,11 @@ class P4Submit(Command, P4UserMap): commits.append(line.strip()) commits.reverse() + if self.preserveUser or (gitConfig("git-p4.skipUserNameCheck") == "true"): + self.checkAuthorship = False + else: + self.checkAuthorship = True + if self.preserveUser: self.checkValidP4Users(commits) diff --git a/contrib/fast-import/git-p4.txt b/contrib/fast-import/git-p4.txt index b6986f0eb..caa4bb3e3 100644 --- a/contrib/fast-import/git-p4.txt +++ b/contrib/fast-import/git-p4.txt @@ -225,6 +225,13 @@ stop. With allowMissingPerforceUsers set to true, git-p4 will use the current user (i.e. the behavior without --preserve-user) and carry on with the perforce commit. +git-p4.skipUserNameCheck + + git config [--global] git-p4.skipUserNameCheck false + +When submitting, git-p4 checks that the git commits are authored by the current +p4 user, and warns if they are not. This disables the check. + Implementation Details... ========================= diff --git a/t/t9800-git-p4.sh b/t/t9800-git-p4.sh index 4fb0e24f8..33b012765 100755 --- a/t/t9800-git-p4.sh +++ b/t/t9800-git-p4.sh @@ -160,6 +160,13 @@ p4_check_commit_author() { fi } +make_change_by_user() { + file=$1 name=$2 email=$3 && + echo "username: a change by $name" >>"$file" && + git add "$file" && + git commit --author "$name <$email>" -m "a change by $name" +} + # Test username support, submitting as user 'alice' test_expect_success 'preserve users' ' p4_add_user alice Alice && @@ -213,6 +220,40 @@ test_expect_success 'preserve user where author is unknown to p4' ' rm -rf "$git" && mkdir "$git" ' +# If we're *not* using --preserve-user, git-p4 should warn if we're submitting +# changes that are not all ours. +# Test: user in p4 and user unknown to p4. +# Test: warning disabled and user is the same. +test_expect_success 'not preserving user with mixed authorship' ' + "$GITP4" clone --dest="$git" //depot && + ( + cd "$git" && + git config git-p4.skipSubmitEditCheck true && + p4_add_user derek Derek && + + make_change_by_user usernamefile3 Derek derek@localhost && + P4EDITOR=cat P4USER=alice P4PASSWD=secret "$GITP4" commit >actual && + grep "git author derek@localhost does not match" actual && + + make_change_by_user usernamefile3 Charlie charlie@localhost && + P4EDITOR=cat P4USER=alice P4PASSWD=secret "$GITP4" commit >actual && + grep "git author charlie@localhost does not match" actual && + + make_change_by_user usernamefile3 alice alice@localhost && + P4EDITOR=cat P4USER=alice P4PASSWD=secret "$GITP4" commit >actual && + ! grep "git author.*does not match" actual && + + git config git-p4.skipUserNameCheck true && + make_change_by_user usernamefile3 Charlie charlie@localhost && + P4EDITOR=cat P4USER=alice P4PASSWD=secret "$GITP4" commit >actual && + ! grep "git author.*does not match" actual && + + p4_check_commit_author usernamefile3 alice + ) && + rm -rf "$git" && mkdir "$git" +' + + test_expect_success 'shutdown' ' pid=`pgrep -f p4d` && test -n "$pid" &&