ATLAS Offline Software
git-package-pseudomerge.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 
4 # Copyright (C) 2002-2018 CERN for the benefit of the ATLAS collaboration
5 # @file: git-package-pseudomerge.py
6 # @author: Tim Martin
7 #
8 
9 """
10 Merge individual Athena packages between git branches. Commit the resulting diff as a cherry-pick.
11 """
12 
13 __author__ = "Tim Martin"
14 __version__ = "$Revision: 1.0 $"
15 __doc__ = "Merge individual Athena packages between git branches. Commit the resulting diff as a cherry-pick."
16 
17 import argparse, time
18 
19 parser = argparse.ArgumentParser(description='\
20  Perform a pseudo-merge (a selective merge followed by cherry-pick) of select packages between branches in Athena. \
21  This performs a proper merge, using the full history in both the source and target branches but restricting to select packages (paths). \
22  The result of this operation, after conflicts are resolved, is isolated as a cherry-pick which is used to apply the changes to the official code base.')
23 parser.add_argument('--packages', type=str, nargs='+', required=True,
24  help='List of package paths to migrate to the target branch, separated by spaces "Path/To/Package1 Path/To/Package2" etc.')
25 parser.add_argument('--stage', type=int,
26  help='Set initially to 1 to setup the merge and present conflicts, set to 2 to create a branch which will apply the changes of the pseudo-merge')
27 parser.add_argument('--source', type=str,
28  default='upstream/21.0', help='source branch (default: upstream/21.0)')
29 parser.add_argument('--target', type=str,
30  default='upstream/master', help='target branch (default: upstream/master)')
31 parser.add_argument('--reset', action='store_true',
32  help='Remove branches created by this script. Use to start again or recover from any issues')
33 parser.add_argument('--debug', action='store_true',
34  help='Extra printing')
35 
36 args = parser.parse_args()
37 
38 def sortBySplitLen(element):
39  return len(element.split('/'))
40 
41 args.packages = sorted(args.packages, key=sortBySplitLen)
42 
43 print(args)
44 
45 class bcolors:
46  HEADER = '\033[95m'
47  OKBLUE = '\033[94m'
48  OKGREEN = '\033[92m'
49  WARNING = '\033[93m'
50  FAIL = '\033[91m'
51  ENDC = '\033[0m'
52  BOLD = '\033[1m'
53  UNDERLINE = '\033[4m'
54 
55 try:
56  from subprocess import call, STDOUT, check_output
57  import os
58 except:
59  print(bcolors.FAIL + 'Cannot import subprocess or os, please make sure you have an OKish Python version. "asetup Athena,master,latest && lsetup git" will set you up.' + bcolors.ENDC)
60  exit()
61 
62 if call(["git", "branch"], stderr=STDOUT, stdout=open(os.devnull, 'w')) != 0:
63  print(bcolors.FAIL + 'Must be called from within a clone of Athena. If you are in your athena directory then please double check that you have run "lsetup git".' + bcolors.ENDC)
64  exit()
65 
66 import time
67 userBranch = os.environ['USER'] + "_" + time.strftime('%d_%b') + "_" + args.packages[0].rstrip('/').split('/')[-1] + "_to_" + args.target.split('/')[-1]
68 userTempBranch = userBranch + "_TEMP_BRANCH_DO_NOT_MERGE_TO_OFFICIAL_REPOSITORY"
69 
70 if call(["git", "branch"], stderr=STDOUT, stdout=open(os.devnull, 'w')) != 0:
71  print(bcolors.FAIL + 'Must be called from within a clone of Athena' + bcolors.ENDC)
72  exit()
73 
74 if args.reset:
75  print(bcolors.HEADER + 'Removing branches created by this script.' + bcolors.ENDC)
76  print(bcolors.HEADER + 'Checking out ' + args.target + bcolors.ENDC)
77  call(["rm", "-f", ".git/index.lock"])
78  print(bcolors.OKBLUE + 'git checkout --force ' + args.target + bcolors.ENDC)
79  call(["git", "checkout", "--force", args.target])
80  time.sleep(0.1)
81  print(bcolors.OKBLUE + 'git reset --hard HEAD' + bcolors.ENDC)
82  call(["git", "reset", "--hard", "HEAD"])
83  time.sleep(0.1)
84  print(bcolors.OKBLUE + 'git clean -fdx' + bcolors.ENDC)
85  call(["git", "clean", "-fdx"])
86  time.sleep(0.1)
87  #
88  print(bcolors.HEADER + 'Delete local branch ' + userBranch + bcolors.ENDC)
89  print(bcolors.OKBLUE + 'git branch -D ' + userBranch + bcolors.ENDC)
90  call(["git", "branch", "-D", userBranch])
91  time.sleep(0.1)
92  #
93  print(bcolors.HEADER + 'Delete remote branch ' + userBranch + bcolors.ENDC)
94  print(bcolors.OKBLUE + 'git push origin --delete ' + userBranch + bcolors.ENDC)
95  call(["git", "push", "origin", "--delete", userBranch])
96  time.sleep(0.1)
97  #
98  print(bcolors.HEADER + 'Delete local temporary branch ' + userTempBranch + bcolors.ENDC)
99  print(bcolors.OKBLUE + 'git branch -D ' + userBranch + bcolors.ENDC)
100  call(["git", "branch", "-D", userTempBranch])
101  time.sleep(0.1)
102  #
103  print(bcolors.HEADER + 'Reset done. Start again by specifying "--stage 1"' + bcolors.ENDC)
104  exit()
105 
106 try:
107  args.stage
108 except NameError:
109  print(bcolors.FAIL + 'Must specify "--stage 1" or "--stage 2"' + bcolors.ENDC)
110  exit()
111 
112 if args.stage == 1:
113  print(bcolors.HEADER + 'Updating from upstream' + bcolors.ENDC)
114  print(bcolors.OKBLUE + 'git fetch upstream ' + bcolors.ENDC)
115  call(["git", "fetch", "upstream"])
116 
117  # Check we're not going to trample on work
118  localChanges = check_output(["git", "status", "--porcelain"],text=True)
119  if localChanges != "":
120  print("Local changes: " + localChanges)
121  prompt = input(bcolors.WARNING + 'This will discard all local changes in the repository which are not already committed, please confirm this is OK! (y/n): ' + bcolors.ENDC)
122  if (prompt != "Y" and prompt != "y"):
123  print(bcolors.HEADER + 'Exiting' + bcolors.ENDC)
124  exit()
125 
126  print(bcolors.HEADER + 'Checking out ' + args.target + ' and discarding any un-committed changes' + bcolors.ENDC)
127 
128  # Try and cover all cases - start in a good place
129  call(["rm", "-f", ".git/index.lock"])
130  print(bcolors.OKBLUE + 'git checkout --force ' + args.target + bcolors.ENDC)
131  call(["git", "checkout", "--force", args.target])
132  time.sleep(0.1)
133  #
134  print(bcolors.OKBLUE + 'git clean -fdx' + bcolors.ENDC)
135  call(["git", "clean", "-fdx"])
136  time.sleep(0.1)
137 
138  print(bcolors.HEADER + 'Creating new temporary branch: ' + userTempBranch + bcolors.ENDC)
139 
140  # Make the new branch
141  print(bcolors.OKBLUE + 'git branch ' + userTempBranch + ' ' + args.target + bcolors.ENDC)
142  call(["git", "branch", userTempBranch, args.target])
143  time.sleep(0.1)
144  print(bcolors.OKBLUE + 'git checkout ' + userTempBranch + bcolors.ENDC)
145  call(["git", "checkout", userTempBranch])
146  time.sleep(0.1)
147 
148  print(bcolors.HEADER + 'Performing merge (this may take a while...)' + bcolors.ENDC)
149  print(bcolors.OKBLUE + 'git merge --no-commit ' + args.source + bcolors.ENDC)
150  call(["git", "merge", "--no-commit", args.source])
151  time.sleep(0.1)
152 
153  print(bcolors.HEADER + 'Performing reset ' + bcolors.ENDC)
154 
155  result = check_output(["git", "status", "--porcelain"],text=True)
156  toReset = []
157  responsibleRule = ""
158 
159  for line in result.splitlines():
160  fileTuple = line.split() # Git status flag and path
161  fileSplit = fileTuple[1].split('/') # Exploded path
162  level = 0 # Distance into directory tree
163  resetPath = ''
164  while (True):
165  if (args.debug): print("Evaluate " + fileTuple[1] + " level=" + str(level))
166  if level == len(fileSplit): # Reached beyond bottom of directory tree
167  print(bcolors.FAIL + "ERR" + bcolors.ENDC)
168  break
169 
170  doKeep = False
171  doProgress = False
172 
173  for path in args.packages: # For each package in the list of merge-packages
174  pathSplit = path.rstrip('/').split('/') # Explode path
175  if level == len(pathSplit) and path == responsibleRule:
176  doKeep = True # The previous operation matched the final path segment. Keep this file
177  if (args.debug): print(" doKeep=True (" + path + " = " + responsibleRule + ") as level is equal to size of " + path + ", " + str(len(pathSplit)) )
178  elif doKeep == False and level < len(pathSplit) and fileSplit[level] == pathSplit[level]:
179  doProgress = True # We match the current level - check next level of directory
180  responsibleRule = path # Record which package rule is allowing us to progress
181  if (args.debug): print(" doProgress=True (" + fileSplit[level] + " = " + pathSplit[level] + ") as level is less than the size of " + path + ", " + str(len(pathSplit)) )
182 
183  if doKeep: # At least one match to keep this
184  if (args.debug): print(" Keeping "+path)
185  break
186  elif doProgress: # At least one partial match - check next level
187  resetPath += fileSplit[level] + '/'
188  if (args.debug): print(" Progressing "+resetPath)
189  level += 1
190  else: # No matches - reset this path
191  resetPath += fileSplit[level]
192  if (args.debug): print(" Resetting "+resetPath)
193  if not resetPath in toReset:
194  print("To reset: "+resetPath + " level = " + str(level))
195  toReset.append(resetPath)
196  break
197 
198  for pathToReset in toReset:
199  print("Resetting: " + pathToReset)
200  print(bcolors.OKBLUE + 'git reset HEAD -- ' + pathToReset + bcolors.ENDC)
201  call(["git", "reset", "HEAD", "--", pathToReset], stdout=open(os.devnull, 'w'))
202  time.sleep(0.05)
203  print(bcolors.OKBLUE + 'git checkout HEAD -- ' + pathToReset + bcolors.ENDC)
204  call(["git", "checkout", "HEAD", "--", pathToReset], stdout=open(os.devnull, 'w'))
205  time.sleep(0.05)
206 
207  print(bcolors.HEADER + 'Removing remaining untracked' + bcolors.ENDC)
208  print(bcolors.OKBLUE + 'git clean -fdx' + bcolors.ENDC)
209  call(["git", "clean", "-fdx"])
210  time.sleep(0.1)
211 
212  print(bcolors.HEADER + 'Printing git status' + bcolors.ENDC)
213  print(bcolors.OKBLUE + 'git status' + bcolors.ENDC)
214  call(["git", "status"])
215 
216  print(bcolors.HEADER + 'Please fix any merge conflicts which show up in red in the above "git status" output.' + bcolors.ENDC)
217  print(bcolors.HEADER + 'Please then test recompilation and runtime tests of these (and any dependent) packages against the most recent ' + args.target + ' nightly' + bcolors.ENDC)
218  print(bcolors.HEADER + 'When happy, run this command again with "--stage 2" to prepare the branch which may be merged to the official repository' + bcolors.ENDC)
219  print(bcolors.FAIL + 'Do NOT commit anything yourself, "--stage 2" will handle committing the changes.' + bcolors.ENDC)
220 
221 elif args.stage == 2:
222 
223  conflicted = check_output(["git", "diff", "--name-only", "--diff-filter=U"],text=True)
224  if conflicted != "":
225  print(bcolors.FAIL + "Conflicting paths remain, please resolve and use 'git add' on:\n" + conflicted + bcolors.ENDC)
226  print(bcolors.HEADER + 'Use "git status" to check the state of the merge' + bcolors.ENDC)
227  print(bcolors.HEADER + 'Exiting' + bcolors.ENDC)
228  exit()
229 
230  print(bcolors.HEADER + 'Stashing changes' + bcolors.ENDC)
231  print(bcolors.OKBLUE + 'git stash' + bcolors.ENDC)
232  call(["git", "stash"])
233  time.sleep(0.1)
234 
235  print(bcolors.HEADER + 'Creating new branch: ' + userBranch + bcolors.ENDC)
236  print(bcolors.OKBLUE + 'git checkout -b ' + userBranch + ' ' + args.target + bcolors.ENDC)
237  call(["git", "checkout", "-b", userBranch, args.target])
238  time.sleep(0.1)
239 
240  print(bcolors.HEADER + 'Popping stash' + bcolors.ENDC)
241  print(bcolors.OKBLUE + 'git stash pop' + bcolors.ENDC)
242  call(["git", "stash", "pop"])
243  time.sleep(0.1)
244 
245  print(bcolors.HEADER + 'Committing' + bcolors.ENDC)
246  allPackages = ""
247  for path in args.packages:
248  allPackages += path.rstrip('/').split('/')[-1] + " "
249 
250  commitMessage = "Update packages:" + allPackages + " from " + args.source + " to " + args.target + " via pseudo-merge"
251  print(bcolors.OKBLUE + 'git commit -am "' + commitMessage + '"' + bcolors.ENDC)
252  call(["git", "commit", "-am", commitMessage])
253  time.sleep(0.1)
254 
255  print(bcolors.HEADER + 'Push to user fork and cleanup' + bcolors.ENDC)
256  print(bcolors.OKBLUE + 'git push --set-upstream origin ' + userBranch + bcolors.ENDC)
257  call(["git", "push", "--set-upstream", "origin", userBranch])
258  time.sleep(0.1)
259 
260  print(bcolors.OKBLUE + 'git branch -D ' + userTempBranch + bcolors.ENDC)
261  call(["git", "branch", "-D", userTempBranch])
262 
263  print(bcolors.HEADER + 'Please open a merge request for ' + userBranch + " to " + args.target + bcolors.ENDC)
264  print(bcolors.HEADER + 'https://gitlab.cern.ch/' + os.environ['USER'] + '/athena/merge_requests/new' + bcolors.ENDC)
265  print(bcolors.HEADER + 'Exiting' + bcolors.ENDC)
python.trfUtils.call
def call(args, bufsize=0, executable=None, stdin=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, message="", logger=msg, loglevel=None, timeout=None, retry=2, timefactor=1.5, sleeptime=10)
Definition: trfUtils.py:155
PlotPulseshapeFromCool.input
input
Definition: PlotPulseshapeFromCool.py:106
git-package-pseudomerge.bcolors
Definition: git-package-pseudomerge.py:45
calibdata.exit
exit
Definition: calibdata.py:236
DerivationFramework::TriggerMatchingUtils::sorted
std::vector< typename T::value_type > sorted(T begin, T end)
Helper function to create a sorted vector from an unsorted one.
print
void print(char *figname, TCanvas *c1)
Definition: TRTCalib_StrawStatusPlots.cxx:25
Trk::open
@ open
Definition: BinningType.h:40
str
Definition: BTagTrackIpAccessor.cxx:11
git-package-pseudomerge.sortBySplitLen
def sortBySplitLen(element)
Definition: git-package-pseudomerge.py:38
Trk::split
@ split
Definition: LayerMaterialProperties.h:38