@@ -166,39 +166,57 @@ def remove_unused_packages(self, repo_dict):
166166 self .client .do_delete ('/repos/%s/packages' % repo_name , data = {'PackageRefs' : packages })
167167
168168 def cleanup_snapshots (self ):
169- snapshots = self . client . do_get ( '/snapshots' , { 'sort' : 'time' })
170- exclude = []
169+ # requesting graph.dot always works, even if graphviz (dot) is not installed
170+ dot_data = self . client . do_get ( '/graph.dot' )
171171
172- # Add currently published snapshots into exclude list
173- publishes = self .client .do_get ('/publish' )
174- for publish in publishes :
175- exclude .extend (
176- [x ['Name' ] for x in publish ['Sources' ]]
177- )
172+ # extract edges from dot-data
173+ # edges = list of pairs with (node_id, node_id)
174+ edges = re .findall ('"([^"]+)"->"([^"]+)";' , dot_data )
178175
179- # Add last snapshots into exclude list
180- # TODO: ignore snapshots that are source for merged snapshots
181- snapshot_latest = []
182- for snapshot in snapshots :
183- base_name = snapshot ['Name' ].split ('-' )[0 ]
184- if base_name not in snapshot_latest :
185- snapshot_latest .append (base_name )
186- if snapshot ['Name' ] not in exclude :
187- lg .debug ("Not deleting latest snapshot %s" % snapshot ['Name' ])
188- exclude .append (snapshot ['Name' ])
176+ # extract nodes from dot-data
177+ # list of tuples with (node_id, node_type, node_name)
178+ # node_type is one of 'Repo'|'Snapshot'|'Publish'
179+ nodes_raw = re .findall ('[ \t ]+"([^"]+)".*label="{(Repo|Snapshot|Published) ([^|]+)[^\}"]+}"' , dot_data )
189180
190- exclude = self .list_uniq (exclude )
181+ # convert nodes_raw into nodes
182+ # nodes = dict of node_id to node
183+ # node = (node_type, node_name)
184+ nodes = {node_id : (node_type , node_name ) for node_id , node_type , node_name in nodes_raw }
191185
192- for snapshot in snapshots :
193- if snapshot ['Name' ] not in exclude :
194- lg .info ("Deleting snapshot %s" % snapshot ['Name' ])
195- try :
196- self .client .do_delete ('/snapshots/%s' % snapshot ['Name' ])
197- except AptlyException as e :
198- if e .res .status_code == 409 :
199- lg .warning ("Snapshot %s is being used, can't delete" % snapshot ['Name' ])
200- else :
201- raise
186+ # start with published and flood fill other nodes
187+ published_node_ids = [key for key in nodes if nodes [key ][0 ] == 'Published' ]
188+
189+ # do flood fill
190+ while True :
191+ # get a list of source node_ids of incoming edges
192+ incoming_node_ids = [edge_from for edge_from , edge_to in edges if edge_to in published_node_ids and edge_from not in published_node_ids ]
193+
194+ # add incoming node_ids
195+ published_node_ids .extend (incoming_node_ids )
196+
197+ # no new nodes, we are finished
198+ if len (incoming_node_ids ) == 0 :
199+ break
200+
201+ # we got published nodes, invert the set
202+ non_published_node_ids = [node_id for node_id in nodes if node_id not in published_node_ids ]
203+
204+ # list of nodes with (node_type, node_name)
205+ unreleased_nodes = [nodes [node_id ] for node_id in non_published_node_ids ]
206+
207+ # use only nodes of type 'Snapshot'
208+ unreleased_snapshots = [node [1 ] for node in unreleased_nodes if node [0 ] == 'Snapshot' ]
209+
210+ # actually delete snapshots that are not published
211+ for snapshot in unreleased_snapshots :
212+ lg .info ("Deleting snapshot %s" % snapshot )
213+ try :
214+ self .client .do_delete ('/snapshots/%s' % snapshot )
215+ except AptlyException as e :
216+ if e .res .status_code == 409 :
217+ lg .warning ("Snapshot %s is being used, can't delete" % snapshot )
218+ else :
219+ raise
202220
203221
204222class Publish (object ):
0 commit comments