@matheusbsbs / matheus-bagliano / commits / 3ea2cfd488
Fork 0

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 <luke@diamand.org> Acked-by: Pete Wyckoff <pw@padd.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Luke Diamand committed Apr 21, 2011 at 20:50 UTC 3ea2cfd488ed70e03d8d6f10e601784926db8008
3 files changed +254 -38
contrib/fast-import/git-p4
+141 -38
@@ -474,6 +474,47 @@ class Command:
474 self.usage = "usage: %prog [options]"
475 self.needsGit = True
476
477 +class P4UserMap:
478 + def __init__(self):
479 + self.userMapFromPerforceServer = False
480 +
481 + def getUserCacheFilename(self):
482 + home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
483 + return home + "/.gitp4-usercache.txt"
484 +
485 + def getUserMapFromPerforceServer(self):
486 + if self.userMapFromPerforceServer:
487 + return
488 + self.users = {}
489 + self.emails = {}
490 +
491 + for output in p4CmdList("users"):
492 + if not output.has_key("User"):
493 + continue
494 + self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
495 + self.emails[output["Email"]] = output["User"]
496 +
497 +
498 + s = ''
499 + for (key, val) in self.users.items():
500 + s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
501 +
502 + open(self.getUserCacheFilename(), "wb").write(s)
503 + self.userMapFromPerforceServer = True
504 +
505 + def loadUserMapFromCache(self):
506 + self.users = {}
507 + self.userMapFromPerforceServer = False
508 + try:
509 + cache = open(self.getUserCacheFilename(), "rb")
510 + lines = cache.readlines()
511 + cache.close()
512 + for line in lines:
513 + entry = line.strip().split("\t")
514 + self.users[entry[0]] = entry[1]
515 + except IOError:
516 + self.getUserMapFromPerforceServer()
517 +
518 class P4Debug(Command):
519 def __init__(self):
520 Command.__init__(self)
@@ -554,13 +595,16 @@ class P4RollBack(Command):
595
596 return True
597
557 -class P4Submit(Command):
598 +class P4Submit(Command, P4UserMap):
599 def __init__(self):
600 Command.__init__(self)
601 + P4UserMap.__init__(self)
602 self.options = [
603 optparse.make_option("--verbose", dest="verbose", action="store_true"),
604 optparse.make_option("--origin", dest="origin"),
605 optparse.make_option("-M", dest="detectRenames", action="store_true"),
606 + # preserve the user, requires relevant p4 permissions
607 + optparse.make_option("--preserve-user", dest="preserveUser", action="store_true"),
608 ]
609 self.description = "Submit changes from git to the perforce depot."
610 self.usage += " [name of git branch to submit into perforce depot]"
@@ -568,6 +612,7 @@ class P4Submit(Command):
612 self.origin = ""
613 self.detectRenames = False
614 self.verbose = False
615 + self.preserveUser = gitConfig("git-p4.preserveUser").lower() == "true"
616 self.isWindows = (platform.system() == "Windows")
617
618 def check(self):
@@ -602,6 +647,75 @@ class P4Submit(Command):
647
648 return result
649
650 + def p4UserForCommit(self,id):
651 + # Return the tuple (perforce user,git email) for a given git commit id
652 + self.getUserMapFromPerforceServer()
653 + gitEmail = read_pipe("git log --max-count=1 --format='%%ae' %s" % id)
654 + gitEmail = gitEmail.strip()
655 + if not self.emails.has_key(gitEmail):
656 + return (None,gitEmail)
657 + else:
658 + return (self.emails[gitEmail],gitEmail)
659 +
660 + def checkValidP4Users(self,commits):
661 + # check if any git authors cannot be mapped to p4 users
662 + for id in commits:
663 + (user,email) = self.p4UserForCommit(id)
664 + if not user:
665 + msg = "Cannot find p4 user for email %s in commit %s." % (email, id)
666 + if gitConfig('git-p4.allowMissingP4Users').lower() == "true":
667 + print "%s" % msg
668 + else:
669 + die("Error: %s\nSet git-p4.allowMissingP4Users to true to allow this." % msg)
670 +
671 + def lastP4Changelist(self):
672 + # Get back the last changelist number submitted in this client spec. This
673 + # then gets used to patch up the username in the change. If the same
674 + # client spec is being used by multiple processes then this might go
675 + # wrong.
676 + results = p4CmdList("client -o") # find the current client
677 + client = None
678 + for r in results:
679 + if r.has_key('Client'):
680 + client = r['Client']
681 + break
682 + if not client:
683 + die("could not get client spec")
684 + results = p4CmdList("changes -c %s -m 1" % client)
685 + for r in results:
686 + if r.has_key('change'):
687 + return r['change']
688 + die("Could not get changelist number for last submit - cannot patch up user details")
689 +
690 + def modifyChangelistUser(self, changelist, newUser):
691 + # fixup the user field of a changelist after it has been submitted.
692 + changes = p4CmdList("change -o %s" % changelist)
693 + for c in changes:
694 + if c.has_key('User'):
695 + c['User'] = newUser
696 + input = marshal.dumps(changes[0])
697 + result = p4CmdList("change -f -i", stdin=input)
698 + for r in result:
699 + if r.has_key('code'):
700 + if r['code'] == 'error':
701 + die("Could not modify user field of changelist %s to %s:%s" % (changelist, newUser, r['data']))
702 + if r.has_key('data'):
703 + print("Updated user field for changelist %s to %s" % (changelist, newUser))
704 + return
705 + die("Could not modify user field of changelist %s to %s" % (changelist, newUser))
706 +
707 + def canChangeChangelists(self):
708 + # check to see if we have p4 admin or super-user permissions, either of
709 + # which are required to modify changelists.
710 + results = p4CmdList("-G protects %s" % self.depotPath)
711 + for r in results:
712 + if r.has_key('perm'):
713 + if r['perm'] == 'admin':
714 + return 1
715 + if r['perm'] == 'super':
716 + return 1
717 + return 0
718 +
719 def prepareSubmitTemplate(self):
720 # remove lines in the Files section that show changes to files outside the depot path we're committing into
721 template = ""
@@ -631,6 +745,9 @@ class P4Submit(Command):
745 def applyCommit(self, id):
746 print "Applying %s" % (read_pipe("git log --max-count=1 --pretty=oneline %s" % id))
747
748 + if self.preserveUser:
749 + (p4User, gitEmail) = self.p4UserForCommit(id)
750 +
751 if not self.detectRenames:
752 # If not explicitly set check the config variable
753 self.detectRenames = gitConfig("git-p4.detectRenames").lower() == "true"
@@ -781,8 +898,13 @@ class P4Submit(Command):
898 editor = read_pipe("git var GIT_EDITOR").strip()
899 system(editor + " " + fileName)
900
901 + if gitConfig("git-p4.skipSubmitEditCheck") == "true":
902 + checkModTime = False
903 + else:
904 + checkModTime = True
905 +
906 response = "y"
785 - if os.stat(fileName).st_mtime <= mtime:
907 + if checkModTime and (os.stat(fileName).st_mtime <= mtime):
908 response = "x"
909 while response != "y" and response != "n":
910 response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
@@ -795,6 +917,14 @@ class P4Submit(Command):
917 if self.isWindows:
918 submitTemplate = submitTemplate.replace("\r\n", "\n")
919 p4_write_pipe("submit -i", submitTemplate)
920 +
921 + if self.preserveUser:
922 + if p4User:
923 + # Get last changelist number. Cannot easily get it from
924 + # the submit command output as the output is unmarshalled.
925 + changelist = self.lastP4Changelist()
926 + self.modifyChangelistUser(changelist, p4User)
927 +
928 else:
929 for f in editedFiles:
930 p4_system("revert \"%s\"" % f);
@@ -831,6 +961,10 @@ class P4Submit(Command):
961 if len(self.origin) == 0:
962 self.origin = upstream
963
964 + if self.preserveUser:
965 + if not self.canChangeChangelists():
966 + die("Cannot preserve user names without p4 super-user or admin permissions")
967 +
968 if self.verbose:
969 print "Origin branch is " + self.origin
970
@@ -858,6 +992,9 @@ class P4Submit(Command):
992 commits.append(line.strip())
993 commits.reverse()
994
995 + if self.preserveUser:
996 + self.checkValidP4Users(commits)
997 +
998 while len(commits) > 0:
999 commit = commits[0]
1000 commits = commits[1:]
@@ -877,11 +1014,12 @@ class P4Submit(Command):
1014
1015 return True
1016
880 -class P4Sync(Command):
1017 +class P4Sync(Command, P4UserMap):
1018 delete_actions = ( "delete", "move/delete", "purge" )
1019
1020 def __init__(self):
1021 Command.__init__(self)
1022 + P4UserMap.__init__(self)
1023 self.options = [
1024 optparse.make_option("--branch", dest="branch"),
1025 optparse.make_option("--detect-branches", dest="detectBranches", action="store_true"),
@@ -1236,41 +1374,6 @@ class P4Sync(Command):
1374 print ("Tag %s does not match with change %s: file count is different."
1375 % (labelDetails["label"], change))
1376
1239 - def getUserCacheFilename(self):
1240 - home = os.environ.get("HOME", os.environ.get("USERPROFILE"))
1241 - return home + "/.gitp4-usercache.txt"
1242 -
1243 - def getUserMapFromPerforceServer(self):
1244 - if self.userMapFromPerforceServer:
1245 - return
1246 - self.users = {}
1247 -
1248 - for output in p4CmdList("users"):
1249 - if not output.has_key("User"):
1250 - continue
1251 - self.users[output["User"]] = output["FullName"] + " <" + output["Email"] + ">"
1252 -
1253 -
1254 - s = ''
1255 - for (key, val) in self.users.items():
1256 - s += "%s\t%s\n" % (key.expandtabs(1), val.expandtabs(1))
1257 -
1258 - open(self.getUserCacheFilename(), "wb").write(s)
1259 - self.userMapFromPerforceServer = True
1260 -
1261 - def loadUserMapFromCache(self):
1262 - self.users = {}
1263 - self.userMapFromPerforceServer = False
1264 - try:
1265 - cache = open(self.getUserCacheFilename(), "rb")
1266 - lines = cache.readlines()
1267 - cache.close()
1268 - for line in lines:
1269 - entry = line.strip().split("\t")
1270 - self.users[entry[0]] = entry[1]
1271 - except IOError:
1272 - self.getUserMapFromPerforceServer()
1273 -
1377 def getLabels(self):
1378 self.labels = {}
1379
contrib/fast-import/git-p4.txt
+29
@@ -110,6 +110,12 @@ is not your current git branch you can also pass that as an argument:
110
111 You can override the reference branch with the --origin=mysourcebranch option.
112
113 +The Perforce changelists will be created with the user who ran git-p4. If you
114 +use --preserve-user then git-p4 will attempt to create Perforce changelists
115 +with the Perforce user corresponding to the git commit author. You need to
116 +have sufficient permissions within Perforce, and the git users need to have
117 +Perforce accounts. Permissions can be granted using 'p4 protect'.
118 +
119 If a submit fails you may have to "p4 resolve" and submit manually. You can
120 continue importing the remaining changes with
121
@@ -196,6 +202,29 @@ able to find the relevant client. This client spec will be used to
202 both filter the files cloned by git and set the directory layout as
203 specified in the client (this implies --keep-path style semantics).
204
205 +git-p4.skipSubmitModTimeCheck
206 +
207 + git config [--global] git-p4.skipSubmitModTimeCheck false
208 +
209 +If true, submit will not check if the p4 change template has been modified.
210 +
211 +git-p4.preserveUser
212 +
213 + git config [--global] git-p4.preserveUser false
214 +
215 +If true, attempt to preserve user names by modifying the p4 changelists. See
216 +the "--preserve-user" submit option.
217 +
218 +git-p4.allowMissingPerforceUsers
219 +
220 + git config [--global] git-p4.allowMissingP4Users false
221 +
222 +If git-p4 is setting the perforce user for a commit (--preserve-user) then
223 +if there is no perforce user corresponding to the git author, git-p4 will
224 +stop. With allowMissingPerforceUsers set to true, git-p4 will use the
225 +current user (i.e. the behavior without --preserve-user) and carry on with
226 +the perforce commit.
227 +
228 Implementation Details...
229 =========================
230
t/t9800-git-p4.sh
+84
@@ -12,6 +12,8 @@ test_description='git-p4 tests'
12 GITP4=$GIT_BUILD_DIR/contrib/fast-import/git-p4
13 P4DPORT=10669
14
15 +export P4PORT=localhost:$P4DPORT
16 +
17 db="$TRASH_DIRECTORY/db"
18 cli="$TRASH_DIRECTORY/cli"
19 git="$TRASH_DIRECTORY/git"
@@ -129,6 +131,88 @@ test_expect_success 'clone bare' '
131 rm -rf "$git" && mkdir "$git"
132 '
133
134 +p4_add_user() {
135 + name=$1
136 + fullname=$2
137 + p4 user -f -i <<EOF &&
138 +User: $name
139 +Email: $name@localhost
140 +FullName: $fullname
141 +EOF
142 + p4 passwd -P secret $name
143 +}
144 +
145 +p4_grant_admin() {
146 + name=$1
147 + p4 protect -o |\
148 + awk "{print}END{print \" admin user $name * //depot/...\"}" |\
149 + p4 protect -i
150 +}
151 +
152 +p4_check_commit_author() {
153 + file=$1
154 + user=$2
155 + if p4 changes -m 1 //depot/$file | grep $user > /dev/null ; then
156 + return 0
157 + else
158 + echo "file $file not modified by user $user" 1>&2
159 + return 1
160 + fi
161 +}
162 +
163 +# Test username support, submitting as user 'alice'
164 +test_expect_success 'preserve users' '
165 + p4_add_user alice Alice &&
166 + p4_add_user bob Bob &&
167 + p4_grant_admin alice &&
168 + "$GITP4" clone --dest="$git" //depot &&
169 + cd "$git" &&
170 + echo "username: a change by alice" >> file1 &&
171 + echo "username: a change by bob" >> file2 &&
172 + git commit --author "Alice <alice@localhost>" -m "a change by alice" file1 &&
173 + git commit --author "Bob <bob@localhost>" -m "a change by bob" file2 &&
174 + git config git-p4.skipSubmitEditCheck true &&
175 + P4EDITOR=touch P4USER=alice P4PASSWD=secret "$GITP4" commit --preserve-user &&
176 + p4_check_commit_author file1 alice &&
177 + p4_check_commit_author file2 bob &&
178 + cd "$TRASH_DIRECTORY" &&
179 + rm -rf "$git" && mkdir "$git"
180 +'
181 +
182 +# Test username support, submitting as bob, who lacks admin rights. Should
183 +# not submit change to p4 (git diff should show deltas).
184 +test_expect_success 'refuse to preserve users without perms' '
185 + "$GITP4" clone --dest="$git" //depot &&
186 + cd "$git" &&
187 + echo "username-noperms: a change by alice" >> file1 &&
188 + git commit --author "Alice <alice@localhost>" -m "perms: a change by alice" file1 &&
189 + ! P4EDITOR=touch P4USER=bob P4PASSWD=secret "$GITP4" commit --preserve-user &&
190 + ! git diff --exit-code HEAD..p4/master > /dev/null &&
191 + cd "$TRASH_DIRECTORY" &&
192 + rm -rf "$git" && mkdir "$git"
193 +'
194 +
195 +# What happens with unknown author? Without allowMissingP4Users it should fail.
196 +test_expect_success 'preserve user where author is unknown to p4' '
197 + "$GITP4" clone --dest="$git" //depot &&
198 + cd "$git" &&
199 + git config git-p4.skipSubmitEditCheck true
200 + echo "username-bob: a change by bob" >> file1 &&
201 + git commit --author "Bob <bob@localhost>" -m "preserve: a change by bob" file1 &&
202 + echo "username-unknown: a change by charlie" >> file1 &&
203 + git commit --author "Charlie <charlie@localhost>" -m "preserve: a change by charlie" file1 &&
204 + ! P4EDITOR=touch P4USER=alice P4PASSWD=secret "$GITP4" commit --preserve-user &&
205 + ! git diff --exit-code HEAD..p4/master > /dev/null &&
206 + echo "$0: repeat with allowMissingP4Users enabled" &&
207 + git config git-p4.allowMissingP4Users true &&
208 + git config git-p4.preserveUser true &&
209 + P4EDITOR=touch P4USER=alice P4PASSWD=secret "$GITP4" commit &&
210 + git diff --exit-code HEAD..p4/master > /dev/null &&
211 + p4_check_commit_author file1 alice &&
212 + cd "$TRASH_DIRECTORY" &&
213 + rm -rf "$git" && mkdir "$git"
214 +'
215 +
216 test_expect_success 'shutdown' '
217 pid=`pgrep -f p4d` &&
218 test -n "$pid" &&